// // AWTImageTools.java // /* OME Bio-Formats package for reading and converting biological file formats. Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc. This program 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 2 of the License, or (at your option) any later version. This program 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 this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package loci.formats.gui; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Image; import java.awt.MediaTracker; import java.awt.RenderingHints; import java.awt.Transparency; import java.awt.color.ColorSpace; import java.awt.geom.AffineTransform; import java.awt.image.BandedSampleModel; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.ComponentColorModel; import java.awt.image.ComponentSampleModel; import java.awt.image.DataBuffer; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferDouble; import java.awt.image.DataBufferFloat; import java.awt.image.DataBufferInt; import java.awt.image.DataBufferUShort; import java.awt.image.IndexColorModel; import java.awt.image.PixelInterleavedSampleModel; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.SampleModel; import java.awt.image.SinglePixelPackedSampleModel; import java.awt.image.WritableRaster; import java.io.IOException; import java.util.Hashtable; import loci.common.DataTools; import loci.formats.FormatException; import loci.formats.FormatTools; import loci.formats.IFormatReader; import loci.formats.ImageTools; import loci.formats.MetadataTools; import loci.formats.meta.MetadataRetrieve; import ome.xml.model.primitives.PositiveInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A utility class with convenience methods for manipulating images * in {@link java.awt.image.BufferedImage} form. * * To work with images in primitive array form, * use the {@link loci.formats.ImageTools} class. * * Much code was stolen and adapted from * <a href="http://forum.java.sun.com/thread.jspa?threadID=522483"> * DrLaszloJamf's posts</a> * on the Java forums. * * <dl><dt><b>Source code:</b></dt> * <dd><a href="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/gui/AWTImageTools.java">Trac</a>, * <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/gui/AWTImageTools.java;hb=HEAD">Gitweb</a></dd></dl> * * @author Curtis Rueden ctrueden at wisc.edu */ public final class AWTImageTools { // -- Constants -- /** ImageObserver for working with AWT images. */ protected static final Component OBS = new Container(); private static final Logger LOGGER = LoggerFactory.getLogger(AWTImageTools.class); // -- Constructor -- private AWTImageTools() { } // -- Image construction - from 1D (single channel) data arrays -- /** * Creates an image from the given single-channel byte data. * * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. * @param signed Whether the byte values should be treated as signed * (-128 to 127) instead of unsigned (0 to 255). */ public static BufferedImage makeImage(byte[] data, int w, int h, boolean signed) { return makeImage(new byte[][] {data}, w, h, signed); } /** * Creates an image from the given single-channel short data. * * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. * @param signed Whether the short values should be treated as signed * (-32768 to 32767) instead of unsigned (0 to 65535). */ public static BufferedImage makeImage(short[] data, int w, int h, boolean signed) { return makeImage(new short[][] {data}, w, h, signed); } /** * Creates an image from the given single-channel int data. * * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. * @param signed Whether the int values should be treated as signed * (-2^31 to 2^31-1) instead of unsigned (0 to 2^32-1). */ public static BufferedImage makeImage(int[] data, int w, int h, boolean signed) { return makeImage(new int[][] {data}, w, h, signed); } /** * Creates an image from the given single-channel float data. * * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. */ public static BufferedImage makeImage(float[] data, int w, int h) { return makeImage(new float[][] {data}, w, h); } /** * Creates an image from the given single-channel double data. * * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. */ public static BufferedImage makeImage(double[] data, int w, int h) { return makeImage(new double[][] {data}, w, h); } // -- Image construction - from 1D (interleaved or banded) data arrays -- /** * Creates an image from the given byte data. * * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. * @param c Number of channels. * @param interleaved If set, the channels are assumed to be interleaved; * otherwise they are assumed to be sequential. * For example, for RGB data, the pattern "RGBRGBRGB..." is interleaved, * while "RRR...GGG...BBB..." is sequential. * @param signed Whether the byte values should be treated as signed * (-128 to 127) instead of unsigned (0 to 255). */ public static BufferedImage makeImage(byte[] data, int w, int h, int c, boolean interleaved, boolean signed) { if (c == 1) return makeImage(data, w, h, signed); if (c > 2) return makeRGBImage(data, c, w, h, interleaved); int dataType; DataBuffer buffer; dataType = DataBuffer.TYPE_BYTE; if (signed) { buffer = new SignedByteBuffer(data, c * w * h); } else { buffer = new DataBufferByte(data, c * w * h); } return constructImage(c, dataType, w, h, interleaved, false, buffer); } /** * Creates an image from the given short data. * * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. * @param c Number of channels. * @param interleaved If set, the channels are assumed to be interleaved; * otherwise they are assumed to be sequential. * For example, for RGB data, the pattern "RGBRGBRGB..." is interleaved, * while "RRR...GGG...BBB..." is sequential. * @param signed Whether the short values should be treated as signed * (-32768 to 32767) instead of unsigned (0 to 65535). */ public static BufferedImage makeImage(short[] data, int w, int h, int c, boolean interleaved, boolean signed) { if (c == 1) return makeImage(data, w, h, signed); int dataType; DataBuffer buffer; if (signed) { dataType = DataBuffer.TYPE_SHORT; buffer = new SignedShortBuffer(data, c * w * h); } else { dataType = DataBuffer.TYPE_USHORT; buffer = new DataBufferUShort(data, c * w * h); } return constructImage(c, dataType, w, h, interleaved, false, buffer); } /** * Creates an image from the given int data. * * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. * @param c Number of channels. * @param interleaved If set, the channels are assumed to be interleaved; * otherwise they are assumed to be sequential. * For example, for RGB data, the pattern "RGBRGBRGB..." is interleaved, * while "RRR...GGG...BBB..." is sequential. * @param signed Whether the int values should be treated as signed * (-2^31 to 2^31-1) instead of unsigned (0 to 2^32-1). */ public static BufferedImage makeImage(int[] data, int w, int h, int c, boolean interleaved, boolean signed) { if (c == 1) return makeImage(data, w, h, signed); int dataType = DataBuffer.TYPE_INT; DataBuffer buffer; if (signed) { buffer = new DataBufferInt(data, c * w * h); } else { buffer = new UnsignedIntBuffer(data, c * w * h); } return constructImage(c, dataType, w, h, interleaved, false, buffer); } /** * Creates an image from the given float data. * * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. * @param c Number of channels. * @param interleaved If set, the channels are assumed to be interleaved; * otherwise they are assumed to be sequential. * For example, for RGB data, the pattern "RGBRGBRGB..." is interleaved, * while "RRR...GGG...BBB..." is sequential. */ public static BufferedImage makeImage(float[] data, int w, int h, int c, boolean interleaved) { if (c == 1) return makeImage(data, w, h); int dataType = DataBuffer.TYPE_FLOAT; DataBuffer buffer = new DataBufferFloat(data, c * w * h); return constructImage(c, dataType, w, h, interleaved, false, buffer); } /** * Creates an image from the given double data. * * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. * @param c Number of channels. * @param interleaved If set, the channels are assumed to be interleaved; * otherwise they are assumed to be sequential. * For example, for RGB data, the pattern "RGBRGBRGB..." is interleaved, * while "RRR...GGG...BBB..." is sequential. */ public static BufferedImage makeImage(double[] data, int w, int h, int c, boolean interleaved) { if (c == 1) return makeImage(data, w, h); int dataType = DataBuffer.TYPE_DOUBLE; DataBuffer buffer = new DataBufferDouble(data, c * w * h); return constructImage(c, dataType, w, h, interleaved, false, buffer); } // -- Image construction - from 2D (banded) data arrays -- /** * Creates an image from the given byte data. * * @param data Array containing image data. * It is assumed that each channel corresponds to one element of the array. * For example, for RGB data, data[0] is R, data[1] is G, and data[2] is B. * @param w Width of image plane. * @param h Height of image plane. * @param signed Whether the byte values should be treated as signed * (-128 to 127) instead of unsigned (0 to 255). */ public static BufferedImage makeImage(byte[][] data, int w, int h, boolean signed) { if (data.length > 2) return makeRGBImage(data, w, h); int dataType; DataBuffer buffer; dataType = DataBuffer.TYPE_BYTE; if (signed) { buffer = new SignedByteBuffer(data, data[0].length); } else { buffer = new DataBufferByte(data, data[0].length); } return constructImage(data.length, dataType, w, h, false, true, buffer); } /** * Creates an image from the given short data. * * @param data Array containing image data. * It is assumed that each channel corresponds to one element of the array. * For example, for RGB data, data[0] is R, data[1] is G, and data[2] is B. * @param w Width of image plane. * @param h Height of image plane. * @param signed Whether the short values should be treated as signed * (-32768 to 32767) instead of unsigned (0 to 65535). */ public static BufferedImage makeImage(short[][] data, int w, int h, boolean signed) { int dataType; DataBuffer buffer; if (signed) { dataType = DataBuffer.TYPE_SHORT; buffer = new SignedShortBuffer(data, data[0].length); } else { dataType = DataBuffer.TYPE_USHORT; buffer = new DataBufferUShort(data, data[0].length); } return constructImage(data.length, dataType, w, h, false, true, buffer); } /** * Creates an image from the given int data. * * @param data Array containing image data. * It is assumed that each channel corresponds to one element of the array. * For example, for RGB data, data[0] is R, data[1] is G, and data[2] is B. * @param w Width of image plane. * @param h Height of image plane. * @param signed Whether the int values should be treated as signed * (-2^31 to 2^31-1) instead of unsigned (0 to 2^32-1). */ public static BufferedImage makeImage(int[][] data, int w, int h, boolean signed) { int dataType = DataBuffer.TYPE_INT; DataBuffer buffer; if (signed) { buffer = new DataBufferInt(data, data[0].length); } else { buffer = new UnsignedIntBuffer(data, data[0].length); } return constructImage(data.length, dataType, w, h, false, true, buffer); } /** * Creates an image from the given single-precision floating point data. * * @param data Array containing image data. * It is assumed that each channel corresponds to one element of the array. * For example, for RGB data, data[0] is R, data[1] is G, and data[2] is B. * @param w Width of image plane. * @param h Height of image plane. */ public static BufferedImage makeImage(float[][] data, int w, int h) { int dataType = DataBuffer.TYPE_FLOAT; DataBuffer buffer = new DataBufferFloat(data, data[0].length); return constructImage(data.length, dataType, w, h, false, true, buffer); } /** * Creates an image from the given double-precision floating point data. * * @param data Array containing image data. * It is assumed that each channel corresponds to one element of the array. * For example, for RGB data, data[0] is R, data[1] is G, and data[2] is B. * @param w Width of image plane. * @param h Height of image plane. */ public static BufferedImage makeImage(double[][] data, int w, int h) { int dataType = DataBuffer.TYPE_DOUBLE; DataBuffer buffer = new DataBufferDouble(data, data[0].length); return constructImage(data.length, dataType, w, h, false, true, buffer); } // -- Image construction - with type conversion -- /** * Creates an image from the given raw byte array, obtaining the * dimensional parameters from the specified metadata object. * * @param data Array containing image data. * @param interleaved If set, the channels are assumed to be interleaved; * otherwise they are assumed to be sequential. * For example, for RGB data, the pattern "RGBRGBRGB..." is interleaved, * while "RRR...GGG...BBB..." is sequential. * @param meta Metadata object containing dimensional parameters. * @param series Relevant image series number of metadata object. */ public static BufferedImage makeImage(byte[] data, boolean interleaved, MetadataRetrieve meta, int series) throws FormatException { MetadataTools.verifyMinimumPopulated(meta, series); int width = meta.getPixelsSizeX(series).getValue().intValue(); int height = meta.getPixelsSizeY(series).getValue().intValue(); String pixelType = meta.getPixelsType(series).toString(); int type = FormatTools.pixelTypeFromString(pixelType); PositiveInteger nChannels = meta.getChannelSamplesPerPixel(series, 0); if (nChannels == null) { LOGGER.warn("SamplesPerPixel is null; it is assumed to be 1."); } int channels = nChannels == null ? 1 : nChannels.getValue(); boolean littleEndian = !meta.getPixelsBinDataBigEndian(series, 0).booleanValue(); return makeImage(data, width, height, channels, interleaved, FormatTools.getBytesPerPixel(type), FormatTools.isFloatingPoint(type), littleEndian, FormatTools.isSigned(type)); } /** * Creates an image from the given raw byte array, * performing any necessary type conversions. * * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. * @param c Number of channels. * @param interleaved If set, the channels are assumed to be interleaved; * otherwise they are assumed to be sequential. * For example, for RGB data, the pattern "RGBRGBRGB..." is interleaved, * while "RRR...GGG...BBB..." is sequential. * @param bpp Denotes the number of bytes in the returned primitive type * (e.g. if bpp == 2, we should return an array of type short). * @param fp If set and bpp == 4 or bpp == 8, then return floats or doubles. * @param little Whether byte array is in little-endian order. * @param signed Whether the data values should be treated as signed * instead of unsigned. */ public static BufferedImage makeImage(byte[] data, int w, int h, int c, boolean interleaved, int bpp, boolean fp, boolean little, boolean signed) { Object pixels = DataTools.makeDataArray(data, bpp % 3 == 0 ? bpp / 3 : bpp, fp, little); if (pixels instanceof byte[]) { return makeImage((byte[]) pixels, w, h, c, interleaved, signed); } else if (pixels instanceof short[]) { return makeImage((short[]) pixels, w, h, c, interleaved, signed); } else if (pixels instanceof int[]) { return makeImage((int[]) pixels, w, h, c, interleaved, signed); } else if (pixels instanceof float[]) { return makeImage((float[]) pixels, w, h, c, interleaved); } else if (pixels instanceof double[]) { return makeImage((double[]) pixels, w, h, c, interleaved); } return null; } /** * Creates an image from the given raw byte array, * performing any necessary type conversions. * * @param data Array containing image data, one channel per element. * @param w Width of image plane. * @param h Height of image plane. * @param bpp Denotes the number of bytes in the returned primitive type * (e.g. if bpp == 2, we should return an array of type short). * @param fp If set and bpp == 4 or bpp == 8, then return floats or doubles. * @param little Whether byte array is in little-endian order. * @param signed Whether the data values should be treated as signed * instead of unsigned. */ public static BufferedImage makeImage(byte[][] data, int w, int h, int bpp, boolean fp, boolean little, boolean signed) { int c = data.length; Object v = null; for (int i=0; i<c; i++) { Object pixels = DataTools.makeDataArray(data[i], bpp % 3 == 0 ? bpp / 3 : bpp, fp, little); if (pixels instanceof byte[]) { if (v == null) v = new byte[c][]; ((byte[][]) v)[i] = (byte[]) pixels; } else if (pixels instanceof short[]) { if (v == null) v = new short[c][]; ((short[][]) v)[i] = (short[]) pixels; } else if (pixels instanceof int[]) { if (v == null) v = new int[c][]; ((int[][]) v)[i] = (int[]) pixels; } else if (pixels instanceof float[]) { if (v == null) v = new float[c][]; ((float[][]) v)[i] = (float[]) pixels; } else if (pixels instanceof double[]) { if (v == null) v = new double[c][]; ((double[][]) v)[i] = (double[]) pixels; } } if (v instanceof byte[][]) { return makeImage((byte[][]) v, w, h, signed); } else if (v instanceof short[][]) { return makeImage((short[][]) v, w, h, signed); } else if (v instanceof int[][]) { return makeImage((int[][]) v, w, h, signed); } else if (v instanceof float[][]) { return makeImage((float[][]) v, w, h); } else if (v instanceof double[][]) { return makeImage((double[][]) v, w, h); } return null; } public static BufferedImage makeRGBImage(byte[] data, int c, int w, int h, boolean interleaved) { int cc = Math.min(c, 4); // throw away channels beyond 4 int[] buf = new int[data.length / c]; int nBits = (cc - 1) * 8; for (int i=0; i<buf.length; i++) { for (int q=0; q<cc; q++) { if (interleaved) { buf[i] |= ((data[i*c + q] & 0xff) << (nBits - q*8)); } else { buf[i] |= ((data[q*buf.length + i] & 0xff) << (nBits - q*8)); } } } DataBuffer buffer = new DataBufferInt(buf, buf.length); return constructImage(cc, DataBuffer.TYPE_INT, w, h, false, false, buffer); } public static BufferedImage makeRGBImage(byte[][] data, int w, int h) { int[] buf = new int[data[0].length]; int nBits = (data.length - 1) * 8; for (int i=0; i<buf.length; i++) { for (int q=0; q<data.length; q++) { buf[i] |= ((data[q][i] & 0xff) << (nBits - q*8)); } } DataBuffer buffer = new DataBufferInt(buf, buf.length); return constructImage(data.length, DataBuffer.TYPE_INT, w, h, false, false, buffer); } // -- Image construction - miscellaneous -- /** * Creates a blank image with the given dimensions and transfer type. * @param w Width of image plane. * @param h Height of image plane. * @param c Number of channels. * @param type One of the following types:<ul> * <li>FormatTools.INT8 <b>** unsupported for now **</b></li> * <li>FormatTools.UINT8</li> * <li>FormatTools.INT16</li> * <li>FormatTools.UINT16</li> * <li>FormatTools.INT32</li> * <li>FormatTools.UINT32 <b>** unsupported for now **</b></li> * <li>FormatTools.FLOAT</li> * <li>FormatTools.DOUBLE</li> * </ul> */ public static BufferedImage blankImage(int w, int h, int c, int type) { switch (type) { case FormatTools.INT8: return makeImage(new byte[c][w * h], w, h, true); case FormatTools.UINT8: return makeImage(new byte[c][w * h], w, h, false); case FormatTools.INT16: return makeImage(new short[c][w * h], w, h, true); case FormatTools.UINT16: return makeImage(new short[c][w * h], w, h, false); case FormatTools.INT32: return makeImage(new int[c][w * h], w, h, true); case FormatTools.UINT32: return makeImage(new int[c][w * h], w, h, false); case FormatTools.FLOAT: return makeImage(new float[c][w * h], w, h); case FormatTools.DOUBLE: return makeImage(new double[c][w * h], w, h); } return null; } /** Creates an image with the given DataBuffer. */ public static BufferedImage constructImage(int c, int type, int w, int h, boolean interleaved, boolean banded, DataBuffer buffer) { return constructImage(c, type, w, h, interleaved, banded, buffer, null); } /** Creates an image with the given DataBuffer. */ public static BufferedImage constructImage(int c, int type, int w, int h, boolean interleaved, boolean banded, DataBuffer buffer, ColorModel colorModel) { if (c > 4) { throw new IllegalArgumentException( "Cannot construct image with " + c + " channels"); } if (colorModel == null) { colorModel = makeColorModel(c, type); if (colorModel == null) return null; if (buffer instanceof UnsignedIntBuffer) { try { colorModel = new UnsignedIntColorModel(32, type, c); } catch (IOException e) { return null; } } } SampleModel model; if (c > 2 && type == DataBuffer.TYPE_INT && buffer.getNumBanks() == 1 && !(buffer instanceof UnsignedIntBuffer)) { int[] bitMasks = new int[c]; for (int i=0; i<c; i++) { bitMasks[i] = 0xff << ((c - i - 1) * 8); } model = new SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, w, h, bitMasks); } else if (banded) model = new BandedSampleModel(type, w, h, c); else if (interleaved) { int[] bandOffsets = new int[c]; for (int i=0; i<c; i++) bandOffsets[i] = i; model = new PixelInterleavedSampleModel(type, w, h, c, c * w, bandOffsets); } else { int[] bandOffsets = new int[c]; for (int i=0; i<c; i++) bandOffsets[i] = i * w * h; model = new ComponentSampleModel(type, w, h, 1, w, bandOffsets); } WritableRaster raster = Raster.createWritableRaster(model, buffer, null); BufferedImage b = null; if (c == 1 && type == DataBuffer.TYPE_BYTE && !(buffer instanceof SignedByteBuffer)) { if (colorModel instanceof IndexColorModel) { b = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_INDEXED); } else { b = new BufferedImage(w, h, BufferedImage.TYPE_BYTE_GRAY); } b.setData(raster); } else if (c == 1 && type == DataBuffer.TYPE_USHORT) { if (!(colorModel instanceof IndexColorModel)) { b = new BufferedImage(w, h, BufferedImage.TYPE_USHORT_GRAY); b.setData(raster); } } else if (c > 2 && type == DataBuffer.TYPE_INT && buffer.getNumBanks() == 1 && !(buffer instanceof UnsignedIntBuffer)) { if (c == 3) { b = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); } else if (c == 4) { b = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); } if (b != null) b.setData(raster); } if (b == null) b = new BufferedImage(colorModel, raster, false, null); return b; } /** * Creates an image from the given byte array, using the given * IFormatReader to retrieve additional information. */ public static BufferedImage openImage(byte[] buf, IFormatReader r, int w, int h) throws FormatException, IOException { int pixelType = r.getPixelType(); boolean little = r.isLittleEndian(); boolean normal = r.isNormalized(); int rgbChanCount = r.getRGBChannelCount(); boolean interleaved = r.isInterleaved(); boolean indexed = r.isIndexed(); if (pixelType == FormatTools.FLOAT) { float[] f = (float[]) DataTools.makeDataArray(buf, 4, true, little); if (normal) f = DataTools.normalizeFloats(f); return makeImage(f, w, h, rgbChanCount, interleaved); } else if (pixelType == FormatTools.DOUBLE) { double[] d = (double[]) DataTools.makeDataArray(buf, 8, true, little); if (normal) d = DataTools.normalizeDoubles(d); return makeImage(d, w, h, rgbChanCount, interleaved); } boolean signed = FormatTools.isSigned(pixelType); ColorModel model = null; if (signed) { if (pixelType == FormatTools.INT8) { model = new SignedColorModel(8, DataBuffer.TYPE_BYTE, rgbChanCount); } else if (pixelType == FormatTools.INT16) { model = new SignedColorModel(16, DataBuffer.TYPE_SHORT, rgbChanCount); } else if (pixelType == FormatTools.INT32) { model = new SignedColorModel(32, DataBuffer.TYPE_INT, rgbChanCount); } } int bpp = FormatTools.getBytesPerPixel(pixelType); BufferedImage b = makeImage(buf, w, h, rgbChanCount, interleaved, bpp, false, little, signed); if (b == null) { throw new FormatException("Could not construct BufferedImage"); } if (indexed && rgbChanCount == 1) { if (pixelType == FormatTools.UINT8 || pixelType == FormatTools.INT8) { byte[][] table = r.get8BitLookupTable(); if (table != null && table.length > 0 && table[0] != null) { int len = table[0].length; byte[] dummy = table.length < 3 ? new byte[len] : null; byte[] red = table.length >= 1 ? table[0] : dummy; byte[] green = table.length >= 2 ? table[1] : dummy; byte[] blue = table.length >= 3 ? table[2] : dummy; model = new IndexColorModel(8, len, red, green, blue); } } else if (pixelType == FormatTools.UINT16 || pixelType == FormatTools.INT16) { short[][] table = r.get16BitLookupTable(); if (table != null && table.length > 0 && table[0] != null) { model = new Index16ColorModel(16, table[0].length, table, r.isLittleEndian()); } } } if (model != null) { WritableRaster raster = Raster.createWritableRaster(b.getSampleModel(), b.getRaster().getDataBuffer(), null); b = new BufferedImage(model, raster, false, null); } return b; } // -- Data extraction -- /** * Gets the image's pixel data as arrays of primitives, one per channel. * The returned type will be either byte[][], short[][], int[][], float[][] * or double[][], depending on the image's transfer type. */ public static Object getPixels(BufferedImage image) { return getPixels(image, 0, 0, image.getWidth(), image.getHeight()); } /** * * Gets the image's pixel data as arrays of primitives, one per channel. * The returned type will be either byte[][], short[][], int[][], float[][] * or double[][], depending on the image's transfer type. */ public static Object getPixels(BufferedImage image, int x, int y, int w, int h) { WritableRaster raster = image.getRaster(); return getPixels(raster, x, y, w, h); } /** * Gets the raster's pixel data as arrays of primitives, one per channel. * The returned type will be either byte[][], short[][], int[][], float[][] * or double[][], depending on the raster's transfer type. */ public static Object getPixels(WritableRaster raster) { return getPixels(raster, 0, 0, raster.getWidth(), raster.getHeight()); } /** * Gets the raster's pixel data as arrays of primitives, one per channel. * The returned type will be either byte[][], short[][], int[][], float[][] * or double[][], depending on the raster's transfer type. */ public static Object getPixels(WritableRaster raster, int x, int y, int w, int h) { int tt = raster.getTransferType(); if (tt == DataBuffer.TYPE_BYTE) return getBytes(raster, x, y, w, h); else if (tt == DataBuffer.TYPE_USHORT || tt == DataBuffer.TYPE_SHORT) { return getShorts(raster, x, y, w, h); } else if (tt == DataBuffer.TYPE_INT) return getInts(raster, x, y, w, h); else if (tt == DataBuffer.TYPE_FLOAT) return getFloats(raster, x, y, w, h); else if (tt == DataBuffer.TYPE_DOUBLE) { return getDoubles(raster, x, y, w, h); } else return null; } /** Extracts pixel data as arrays of unsigned bytes, one per channel. */ public static byte[][] getBytes(BufferedImage image) { WritableRaster r = image.getRaster(); return getBytes(r); } /** Extracts pixel data as arrays of unsigned bytes, one per channel. */ public static byte[][] getBytes(WritableRaster r) { return getBytes(r, 0, 0, r.getWidth(), r.getHeight()); } /** Extracts pixel data as arrays of unsigned bytes, one per channel. */ public static byte[][] getBytes(WritableRaster r, int x, int y, int w, int h) { if (canUseBankDataDirectly(r, DataBuffer.TYPE_BYTE, DataBufferByte.class) && x == 0 && y == 0 && w == r.getWidth() && h == r.getHeight()) { return ((DataBufferByte) r.getDataBuffer()).getBankData(); } int c = r.getNumBands(); byte[][] samples = new byte[c][w * h]; int[] buf = new int[w * h]; for (int i=0; i<c; i++) { r.getSamples(x, y, w, h, i, buf); for (int j=0; j<buf.length; j++) samples[i][j] = (byte) buf[j]; } return samples; } /** Extracts pixel data as arrays of unsigned shorts, one per channel. */ public static short[][] getShorts(BufferedImage image) { WritableRaster r = image.getRaster(); return getShorts(r); } /** Extracts pixel data as arrays of unsigned shorts, one per channel. */ public static short[][] getShorts(WritableRaster r) { return getShorts(r, 0, 0, r.getWidth(), r.getHeight()); } /** Extracts pixel data as arrays of unsigned shorts, one per channel. */ public static short[][] getShorts(WritableRaster r, int x, int y, int w, int h) { if (canUseBankDataDirectly(r, DataBuffer.TYPE_USHORT, DataBufferUShort.class) && x == 0 && y == 0 && w == r.getWidth() && h == r.getHeight()) { return ((DataBufferUShort) r.getDataBuffer()).getBankData(); } int c = r.getNumBands(); short[][] samples = new short[c][w * h]; int[] buf = new int[w * h]; for (int i=0; i<c; i++) { r.getSamples(x, y, w, h, i, buf); for (int j=0; j<buf.length; j++) samples[i][j] = (short) buf[j]; } return samples; } /** Extracts pixel data as arrays of signed integers, one per channel. */ public static int[][] getInts(BufferedImage image) { WritableRaster r = image.getRaster(); return getInts(r); } /** Extracts pixel data as arrays of signed integers, one per channel. */ public static int[][] getInts(WritableRaster r) { return getInts(r, 0, 0, r.getWidth(), r.getHeight()); } /** Extracts pixel data as arrays of signed integers, one per channel. */ public static int[][] getInts(WritableRaster r, int x, int y, int w, int h) { if (canUseBankDataDirectly(r, DataBuffer.TYPE_INT, DataBufferInt.class) && x == 0 && y == 0 && w == r.getWidth() && h == r.getHeight()) { return ((DataBufferInt) r.getDataBuffer()).getBankData(); } // NB: an order of magnitude faster than the naive makeType solution int c = r.getNumBands(); int[][] samples = new int[c][w * h]; for (int i=0; i<c; i++) r.getSamples(x, y, w, h, i, samples[i]); return samples; } /** Extracts pixel data as arrays of floats, one per channel. */ public static float[][] getFloats(BufferedImage image) { WritableRaster r = image.getRaster(); return getFloats(r); } /** Extracts pixel data as arrays of floats, one per channel. */ public static float[][] getFloats(WritableRaster r) { return getFloats(r, 0, 0, r.getWidth(), r.getHeight()); } /** Extracts pixel data as arrays of floats, one per channel. */ public static float[][] getFloats(WritableRaster r, int x, int y, int w, int h) { if (canUseBankDataDirectly(r, DataBuffer.TYPE_FLOAT, DataBufferFloat.class) && x == 0 && y == 0 && w == r.getWidth() && h == r.getHeight()) { return ((DataBufferFloat) r.getDataBuffer()).getBankData(); } // NB: an order of magnitude faster than the naive makeType solution int c = r.getNumBands(); float[][] samples = new float[c][w * h]; for (int i=0; i<c; i++) r.getSamples(x, y, w, h, i, samples[i]); return samples; } /** Extracts pixel data as arrays of doubles, one per channel. */ public static double[][] getDoubles(BufferedImage image) { WritableRaster r = image.getRaster(); return getDoubles(r); } /** Extracts pixel data as arrays of doubles, one per channel. */ public static double[][] getDoubles(WritableRaster r) { return getDoubles(r, 0, 0, r.getWidth(), r.getHeight()); } /** Extracts pixel data as arrays of doubles, one per channel. */ public static double[][] getDoubles(WritableRaster r, int x, int y, int w, int h) { if (canUseBankDataDirectly(r, DataBuffer.TYPE_DOUBLE, DataBufferDouble.class) && x == 0 && y == 0 && w == r.getWidth() && h == r.getHeight()) { return ((DataBufferDouble) r.getDataBuffer()).getBankData(); } // NB: an order of magnitude faster than the naive makeType solution int c = r.getNumBands(); double[][] samples = new double[c][w * h]; for (int i=0; i<c; i++) r.getSamples(x, y, w, h, i, samples[i]); return samples; } /** * Whether we can return the data buffer's bank data * without performing any copy or conversion operations. */ private static boolean canUseBankDataDirectly(WritableRaster r, int transferType, Class<? extends DataBuffer> dataBufferClass) { int tt = r.getTransferType(); if (tt != transferType) return false; DataBuffer buffer = r.getDataBuffer(); if (!dataBufferClass.isInstance(buffer)) return false; SampleModel model = r.getSampleModel(); if (!(model instanceof ComponentSampleModel)) return false; ComponentSampleModel csm = (ComponentSampleModel) model; int pixelStride = csm.getPixelStride(); if (pixelStride != 1) return false; int w = r.getWidth(); int scanlineStride = csm.getScanlineStride(); if (scanlineStride != w) return false; int c = r.getNumBands(); int[] bandOffsets = csm.getBandOffsets(); if (bandOffsets.length != c) return false; for (int i=0; i<bandOffsets.length; i++) { if (bandOffsets[i] != 0) return false; } for (int i=0; i<bandOffsets.length; i++) { if (bandOffsets[i] != i) return false; } return true; } /** * Return a 2D array of bytes representing the image. If the transfer type * is something other than DataBuffer.TYPE_BYTE, then each pixel value is * converted to the appropriate number of bytes. In other words, if we * are given an image with 16-bit data, each channel of the resulting array * will have width * height * 2 bytes. */ public static byte[][] getPixelBytes(BufferedImage img, boolean little) { return getPixelBytes(img, little, 0, 0, img.getWidth(), img.getHeight()); } /** * Return a 2D array of bytes representing the image. If the transfer type * is something other than DataBuffer.TYPE_BYTE, then each pixel value is * converted to the appropriate number of bytes. In other words, if we * are given an image with 16-bit data, each channel of the resulting array * will have width * height * 2 bytes. */ public static byte[][] getPixelBytes(WritableRaster r, boolean little) { return getPixelBytes(r, little, 0, 0, r.getWidth(), r.getHeight()); } /** * Return a 2D array of bytes representing the image. If the transfer type * is something other than DataBuffer.TYPE_BYTE, then each pixel value is * converted to the appropriate number of bytes. In other words, if we * are given an image with 16-bit data, each channel of the resulting array * will have width * height * 2 bytes. */ public static byte[][] getPixelBytes(BufferedImage img, boolean little, int x, int y, int w, int h) { Object pixels = getPixels(img, x, y, w, h); int imageType = img.getType(); byte[][] pixelBytes = null; if (pixels instanceof byte[][]) { pixelBytes = (byte[][]) pixels; } else if (pixels instanceof short[][]) { short[][] s = (short[][]) pixels; pixelBytes = new byte[s.length][s[0].length * 2]; for (int i=0; i<pixelBytes.length; i++) { for (int j=0; j<s[0].length; j++) { DataTools.unpackBytes(s[i][j], pixelBytes[i], j * 2, 2, little); } } } else if (pixels instanceof int[][]) { int[][] in = (int[][]) pixels; if (imageType == BufferedImage.TYPE_INT_RGB || imageType == BufferedImage.TYPE_INT_BGR || imageType == BufferedImage.TYPE_INT_ARGB) { pixelBytes = new byte[in.length][in[0].length]; for (int c=0; c<in.length; c++) { for (int i=0; i<in[0].length; i++) { if (imageType != BufferedImage.TYPE_INT_BGR) { pixelBytes[c][i] = (byte) (in[c][i] & 0xff); } else { pixelBytes[in.length - c - 1][i] = (byte) (in[c][i] & 0xff); } } } } else { pixelBytes = new byte[in.length][in[0].length * 4]; for (int i=0; i<pixelBytes.length; i++) { for (int j=0; j<in[0].length; j++) { DataTools.unpackBytes(in[i][j], pixelBytes[i], j * 4, 4, little); } } } } else if (pixels instanceof float[][]) { float[][] in = (float[][]) pixels; pixelBytes = new byte[in.length][in[0].length * 4]; for (int i=0; i<pixelBytes.length; i++) { for (int j=0; j<in[0].length; j++) { int v = Float.floatToIntBits(in[i][j]); DataTools.unpackBytes(v, pixelBytes[i], j * 4, 4, little); } } } else if (pixels instanceof double[][]) { double[][] in = (double[][]) pixels; pixelBytes = new byte[in.length][in[0].length * 8]; for (int i=0; i<pixelBytes.length; i++) { for (int j=0; j<in[0].length; j++) { long v = Double.doubleToLongBits(in[i][j]); DataTools.unpackBytes(v, pixelBytes[i], j * 8, 8, little); } } } return pixelBytes; } /** * Return a 2D array of bytes representing the image. If the transfer type * is something other than DataBuffer.TYPE_BYTE, then each pixel value is * converted to the appropriate number of bytes. In other words, if we * are given an image with 16-bit data, each channel of the resulting array * will have width * height * 2 bytes. */ public static byte[][] getPixelBytes(WritableRaster r, boolean little, int x, int y, int w, int h) { Object pixels = getPixels(r); byte[][] pixelBytes = null; int bpp = 0; if (pixels instanceof byte[][]) { pixelBytes = (byte[][]) pixels; bpp = 1; } else if (pixels instanceof short[][]) { bpp = 2; short[][] s = (short[][]) pixels; pixelBytes = new byte[s.length][s[0].length * bpp]; for (int i=0; i<pixelBytes.length; i++) { for (int j=0; j<s[0].length; j++) { DataTools.unpackBytes(s[i][j], pixelBytes[i], j * bpp, bpp, little); } } } else if (pixels instanceof int[][]) { bpp = 4; int[][] in = (int[][]) pixels; pixelBytes = new byte[in.length][in[0].length * bpp]; for (int i=0; i<pixelBytes.length; i++) { for (int j=0; j<in[0].length; j++) { DataTools.unpackBytes(in[i][j], pixelBytes[i], j * bpp, bpp, little); } } } else if (pixels instanceof float[][]) { bpp = 4; float[][] in = (float[][]) pixels; pixelBytes = new byte[in.length][in[0].length * bpp]; for (int i=0; i<pixelBytes.length; i++) { for (int j=0; j<in[0].length; j++) { int v = Float.floatToIntBits(in[i][j]); DataTools.unpackBytes(v, pixelBytes[i], j * bpp, bpp, little); } } } else if (pixels instanceof double[][]) { bpp = 8; double[][] in = (double[][]) pixels; pixelBytes = new byte[in.length][in[0].length * bpp]; for (int i=0; i<pixelBytes.length; i++) { for (int j=0; j<in[0].length; j++) { long v = Double.doubleToLongBits(in[i][j]); DataTools.unpackBytes(v, pixelBytes[i], j * bpp, bpp, little); } } } if (x == 0 && y == 0 && w == r.getWidth() && h == r.getHeight()) { return pixelBytes; } byte[][] croppedBytes = new byte[pixelBytes.length][w * h * bpp]; for (int c=0; c<croppedBytes.length; c++) { for (int row=0; row<h; row++) { int src = (row + y) * r.getWidth() * bpp + x * bpp; int dest = row * w * bpp; System.arraycopy(pixelBytes[c], src, croppedBytes[c], dest, w * bpp); } } return croppedBytes; } /** * Gets the pixel type of the given image. * @return One of the following types:<ul> * <li>FormatReader.INT8</li> * <li>FormatReader.UINT8</li> * <li>FormatReader.INT16</li> * <li>FormatReader.UINT16</li> * <li>FormatReader.INT32</li> * <li>FormatReader.UINT32</li> * <li>FormatReader.FLOAT</li> * <li>FormatReader.DOUBLE</li> * <li>-1 (unknown type)</li> * </ul> */ public static int getPixelType(BufferedImage image) { final Raster raster = image.getRaster(); if (raster == null) return -1; final DataBuffer buffer = raster.getDataBuffer(); if (buffer == null) return -1; if (buffer instanceof SignedByteBuffer) { return FormatTools.INT8; } else if (buffer instanceof SignedShortBuffer) { return FormatTools.INT16; } else if (buffer instanceof UnsignedIntBuffer) { return FormatTools.UINT32; } int type = buffer.getDataType(); int imageType = image.getType(); switch (type) { case DataBuffer.TYPE_BYTE: return FormatTools.UINT8; case DataBuffer.TYPE_DOUBLE: return FormatTools.DOUBLE; case DataBuffer.TYPE_FLOAT: return FormatTools.FLOAT; case DataBuffer.TYPE_INT: if (imageType == BufferedImage.TYPE_INT_RGB || imageType == BufferedImage.TYPE_INT_BGR || imageType == BufferedImage.TYPE_INT_ARGB) { return FormatTools.UINT8; } if (buffer instanceof UnsignedIntBuffer) { return FormatTools.UINT32; } return FormatTools.INT32; case DataBuffer.TYPE_SHORT: return FormatTools.INT16; case DataBuffer.TYPE_USHORT: if (imageType == BufferedImage.TYPE_USHORT_555_RGB || imageType == BufferedImage.TYPE_USHORT_565_RGB) { return FormatTools.UINT8; } return FormatTools.UINT16; default: return -1; } } // -- Image conversion -- // NB: The commented out makeType method below is broken in that it results // in rescaled data in some circumstances. We were using it for getBytes and // getShorts, but due to this problem we implemented a different solution // using Raster.getPixels instead. But we have left the makeType method here // in case we decide to explore this issue any further in the future. ///** Copies the given image into a result with the specified data type. */ //public static BufferedImage makeType(BufferedImage image, int type) { // WritableRaster r = image.getRaster(); // int w = image.getWidth(), h = image.getHeight(), c = r.getNumBands(); // ColorModel colorModel = makeColorModel(c, type); // if (colorModel == null) return null; // // int s = w * h; // DataBuffer buf = null; // if (type == DataBuffer.TYPE_BYTE) buf = new DataBufferByte(s, c); // else if (type == DataBuffer.TYPE_USHORT) buf = new DataBufferUShort(s, c); // else if (type == DataBuffer.TYPE_INT) buf = new DataBufferInt(s, c); // else if (type == DataBuffer.TYPE_SHORT) buf = new DataBufferShort(s, c); // else if (type == DataBuffer.TYPE_FLOAT) buf = new DataBufferFloat(s, c); // else if (type == DataBuffer.TYPE_DOUBLE) buf = new DataBufferDouble(s, c); // if (buf == null) return null; // // SampleModel model = new BandedSampleModel(type, w, h, c); // WritableRaster raster = Raster.createWritableRaster(model, buf, null); // BufferedImage target = new BufferedImage(colorModel, raster, false, null); // Graphics2D g2 = target.createGraphics(); // g2.drawRenderedImage(image, null); // g2.dispose(); // return target; //} /** * Converts a java.awt.image.RenderedImage into a * java.awt.image.BufferedImage. * * This code was adapted from * <a href="http://www.jguru.com/faq/view.jsp?EID=114602">a jGuru post</a>. */ public static BufferedImage convertRenderedImage(RenderedImage img) { if (img instanceof BufferedImage) return (BufferedImage) img; ColorModel cm = img.getColorModel(); int width = img.getWidth(); int height = img.getHeight(); WritableRaster raster = cm.createCompatibleWritableRaster(width, height); boolean isAlphaPremultiplied = cm.isAlphaPremultiplied(); Hashtable<String, Object> properties = new Hashtable<String, Object>(); String[] keys = img.getPropertyNames(); if (keys != null) { for (int i=0; i<keys.length; i++) { properties.put(keys[i], img.getProperty(keys[i])); } } BufferedImage result = new BufferedImage(cm, raster, isAlphaPremultiplied, properties); img.copyData(raster); return result; } /** Get the bytes from an image, merging the channels as necessary. */ public static byte[] getBytes(BufferedImage img, boolean separated) { byte[][] p = getBytes(img); if (separated || p.length == 1) return p[0]; byte[] rtn = new byte[p.length * p[0].length]; for (int i=0; i<p.length; i++) { System.arraycopy(p[i], 0, rtn, i * p[0].length, p[i].length); } return rtn; } /** * Converts the given BufferedImage into an image with unsigned pixel data. */ public static BufferedImage makeUnsigned(BufferedImage img) { if (img == null) return null; int pixelType = getPixelType(img); boolean signed = FormatTools.isSigned(pixelType); boolean fp = FormatTools.isFloatingPoint(pixelType); if (!signed || fp) return img; int bpp = FormatTools.getBytesPerPixel(pixelType); byte[][] pix = getPixelBytes(img, false); return makeImage(pix, img.getWidth(), img.getHeight(), bpp, fp, false, false); } // -- Image manipulation -- /** Returns a subimage of the specified image. */ public static BufferedImage getSubimage(BufferedImage image, boolean littleEndian, int x, int y, int w, int h) { int pixelType = getPixelType(image); byte[][] pix = getPixelBytes(image, littleEndian, x, y, w, h); return makeImage(pix, w, h, FormatTools.getBytesPerPixel(pixelType), FormatTools.isFloatingPoint(pixelType), littleEndian, FormatTools.isSigned(pixelType)); } /** Splits the given multi-channel image into single-channel images. */ public static BufferedImage[] splitChannels(BufferedImage image) { int w = image.getWidth(), h = image.getHeight(); int c = image.getRaster().getNumBands(); if (c == 1) return new BufferedImage[] {image}; BufferedImage[] results = new BufferedImage[c]; Object o = getPixels(image); int pixelType = getPixelType(image); boolean signed = FormatTools.isSigned(pixelType); if (o instanceof byte[][]) { byte[][] pix = (byte[][]) o; for (int i=0; i<c; i++) results[i] = makeImage(pix[i], w, h, signed); } else if (o instanceof short[][]) { short[][] pix = (short[][]) o; for (int i=0; i<c; i++) results[i] = makeImage(pix[i], w, h, signed); } else if (o instanceof int[][]) { int[][] pix = (int[][]) o; for (int i=0; i<c; i++) results[i] = makeImage(pix[i], w, h, signed); } else if (o instanceof float[][]) { float[][] pix = (float[][]) o; for (int i=0; i<c; i++) results[i] = makeImage(pix[i], w, h); } else if (o instanceof double[][]) { double[][] pix = (double[][]) o; for (int i=0; i<c; i++) results[i] = makeImage(pix[i], w, h); } return results; } /** Merges the given images into a single multi-channel image. */ public static BufferedImage mergeChannels(BufferedImage[] images) { if (images == null || images.length == 0) return null; // create list of pixels arrays Object[] list = new Object[images.length]; int c = 0, type = 0; for (int i=0; i<images.length; i++) { Object o = getPixels(images[i]); if (o instanceof byte[][]) { if (i == 0) type = DataBuffer.TYPE_BYTE; else if (type != DataBuffer.TYPE_BYTE) return null; c += ((byte[][]) o).length; } else if (o instanceof short[][]) { if (i == 0) type = DataBuffer.TYPE_USHORT; else if (type != DataBuffer.TYPE_USHORT) return null; c += ((short[][]) o).length; } else if (o instanceof int[][]) { if (i == 0) type = DataBuffer.TYPE_INT; else if (type != DataBuffer.TYPE_INT) return null; c += ((int[][]) o).length; } else if (o instanceof float[][]) { if (i == 0) type = DataBuffer.TYPE_FLOAT; else if (type != DataBuffer.TYPE_FLOAT) return null; c += ((float[][]) o).length; } else if (o instanceof double[][]) { if (i == 0) type = DataBuffer.TYPE_DOUBLE; else if (type != DataBuffer.TYPE_DOUBLE) return null; c += ((double[][]) o).length; } if (c > 4) return null; list[i] = o; } if (c < 1 || c > 4) return null; // compile results into a single array int w = images[0].getWidth(), h = images[0].getHeight(); int pixelType = getPixelType(images[0]); boolean signed = FormatTools.isSigned(pixelType); if (type == DataBuffer.TYPE_BYTE) { byte[][] pix = new byte[c][]; int ndx = 0; for (int i=0; i<list.length; i++) { byte[][] b = (byte[][]) list[i]; for (int j=0; j<b.length; j++) pix[ndx++] = b[j]; } while (ndx < pix.length) pix[ndx++] = new byte[w * h]; // blank channel return makeImage(pix, w, h, signed); } if (type == DataBuffer.TYPE_USHORT || type == DataBuffer.TYPE_SHORT) { short[][] pix = new short[c][]; int ndx = 0; for (int i=0; i<list.length; i++) { short[][] b = (short[][]) list[i]; for (int j=0; j<b.length; j++) pix[ndx++] = b[j]; } while (ndx < pix.length) pix[ndx++] = new short[w * h]; // blank channel return makeImage(pix, w, h, signed); } if (type == DataBuffer.TYPE_INT) { int[][] pix = new int[c][]; int ndx = 0; for (int i=0; i<list.length; i++) { int[][] b = (int[][]) list[i]; for (int j=0; j<b.length; j++) pix[ndx++] = b[j]; } while (ndx < pix.length) pix[ndx++] = new int[w * h]; // blank channel return makeImage(pix, w, h, signed); } if (type == DataBuffer.TYPE_FLOAT) { float[][] pix = new float[c][]; int ndx = 0; for (int i=0; i<list.length; i++) { float[][] b = (float[][]) list[i]; for (int j=0; j<b.length; j++) pix[ndx++] = b[j]; } while (ndx < pix.length) pix[ndx++] = new float[w * h]; // blank channel return makeImage(pix, w, h); } if (type == DataBuffer.TYPE_DOUBLE) { double[][] pix = new double[c][]; int ndx = 0; for (int i=0; i<list.length; i++) { double[][] b = (double[][]) list[i]; for (int j=0; j<b.length; j++) pix[ndx++] = b[j]; } while (ndx < pix.length) pix[ndx++] = new double[w * h]; // blank channel return makeImage(pix, w, h); } return null; } /** * Pads (or crops) the image to the given width and height. * The image will be centered within the new bounds. */ public static BufferedImage padImage(BufferedImage img, int width, int height) { if (img == null) { byte[][] data = new byte[1][width * height]; return makeImage(data, width, height, false); } boolean needsPadding = img.getWidth() != width || img.getHeight() != height; if (needsPadding) { Object pixels = getPixels(img); int pixelType = getPixelType(img); boolean signed = FormatTools.isSigned(pixelType); if (pixels instanceof byte[][]) { byte[][] b = (byte[][]) pixels; byte[][] newBytes = new byte[b.length][width * height]; for (int i=0; i<b.length; i++) { newBytes[i] = ImageTools.padImage(b[i], false, 1, img.getWidth(), width, height); } return makeImage(newBytes, width, height, signed); } else if (pixels instanceof short[][]) { short[][] b = (short[][]) pixels; short[][] newShorts = new short[b.length][width * height]; for (int i=0; i<b.length; i++) { newShorts[i] = ImageTools.padImage(b[i], false, 1, img.getWidth(), width, height); } return makeImage(newShorts, width, height, signed); } else if (pixels instanceof int[][]) { int[][] b = (int[][]) pixels; int[][] newInts = new int[b.length][width * height]; for (int i=0; i<b.length; i++) { newInts[i] = ImageTools.padImage(b[i], false, 1, img.getWidth(), width, height); } return makeImage(newInts, width, height, signed); } else if (pixels instanceof float[][]) { float[][] b = (float[][]) pixels; float[][] newFloats = new float[b.length][width * height]; for (int i=0; i<b.length; i++) { newFloats[i] = ImageTools.padImage(b[i], false, 1, img.getWidth(), width, height); } return makeImage(newFloats, width, height); } else if (pixels instanceof double[][]) { double[][] b = (double[][]) pixels; double[][] newDoubles = new double[b.length][width * height]; for (int i=0; i<b.length; i++) { newDoubles[i] = ImageTools.padImage(b[i], false, 1, img.getWidth(), width, height); } return makeImage(newDoubles, width, height); } return null; } return img; } /** Perform autoscaling on the given BufferedImage. */ public static BufferedImage autoscale(BufferedImage img) { byte[][] pixels = getPixelBytes(img, true); double min = Double.MAX_VALUE; double max = 0.0; int bits = pixels[0].length / (img.getWidth() * img.getHeight()) * 8; for (int i=0; i<pixels.length; i++) { Double[] mm = ImageTools.scanData(pixels[0], bits, true); double tmin = mm[0].doubleValue(); double tmax = mm[1].doubleValue(); if (tmin < min) min = tmin; if (tmax > max) max = tmax; } return autoscale(img, (int) min, (int) max); } /** * Perform autoscaling on the given BufferedImage; * map min to 0 and max to 255. * If the BufferedImage has 8 bit data, then nothing happens. */ public static BufferedImage autoscale(BufferedImage img, int min, int max) { Object pixels = getPixels(img); int pixelType = getPixelType(img); boolean signed = FormatTools.isSigned(pixelType); if (pixels instanceof byte[][]) return img; else if (pixels instanceof short[][]) { short[][] shorts = (short[][]) pixels; byte[][] out = new byte[shorts.length][shorts[0].length]; for (int i=0; i<out.length; i++) { for (int j=0; j<out[i].length; j++) { if (shorts[i][j] < 0) shorts[i][j] += 32767; int diff = max - min; float dist = (float) (shorts[i][j] - min) / diff; if (shorts[i][j] >= max) out[i][j] = (byte) 255; else if (shorts[i][j] <= min) out[i][j] = 0; else out[i][j] = (byte) (dist * 256); } } return makeImage(out, img.getWidth(), img.getHeight(), signed); } else if (pixels instanceof int[][]) { int[][] ints = (int[][]) pixels; byte[][] out = new byte[ints.length][ints[0].length]; for (int i=0; i<out.length; i++) { for (int j=0; j<out[i].length; j++) { if (ints[i][j] >= max) out[i][j] = (byte) 255; else if (ints[i][j] <= min) out[i][j] = 0; else { int diff = max - min; float dist = (ints[i][j] - min) / diff; out[i][j] = (byte) (dist * 256); } } } return makeImage(out, img.getWidth(), img.getHeight(), signed); } else if (pixels instanceof float[][]) { float[][] floats = (float[][]) pixels; byte[][] out = new byte[floats.length][floats[0].length]; for (int i=0; i<out.length; i++) { for (int j=0; j<out[i].length; j++) { if (floats[i][j] >= max) out[i][j] = (byte) 255; else if (floats[i][j] <= min) out[i][j] = 0; else { int diff = max - min; float dist = (floats[i][j] - min) / diff; out[i][j] = (byte) (dist * 256); } } } return makeImage(out, img.getWidth(), img.getHeight(), signed); } else if (pixels instanceof double[][]) { double[][] doubles = (double[][]) pixels; byte[][] out = new byte[doubles.length][doubles[0].length]; for (int i=0; i<out.length; i++) { for (int j=0; j<out[i].length; j++) { if (doubles[i][j] >= max) out[i][j] = (byte) 255; else if (doubles[i][j] <= min) out[i][j] = 0; else { int diff = max - min; float dist = (float) (doubles[i][j] - min) / diff; out[i][j] = (byte) (dist * 256); } } } return makeImage(out, img.getWidth(), img.getHeight(), signed); } return img; } // -- Image scaling -- /** Copies the source image into the target, applying scaling. */ public static BufferedImage copyScaled(BufferedImage source, BufferedImage target, Object hint) { if (hint == null) hint = RenderingHints.VALUE_INTERPOLATION_BICUBIC; Graphics2D g2 = target.createGraphics(); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint); double scalex = (double) target.getWidth() / source.getWidth(); double scaley = (double) target.getHeight() / source.getHeight(); AffineTransform xform = AffineTransform.getScaleInstance(scalex, scaley); g2.drawRenderedImage(source, xform); g2.dispose(); return target; } /** * Scales the image using the Java2D API, with the resultant * image optimized for the given graphics configuration. */ public static BufferedImage scale2D(BufferedImage image, int width, int height, Object hint, GraphicsConfiguration gc) { if (gc == null) gc = getDefaultConfiguration(); int trans = image.getColorModel().getTransparency(); return copyScaled(image, gc.createCompatibleImage(width, height, trans), hint); } /** * Scales the image using the Java2D API, with the * resultant image having the given color model. */ public static BufferedImage scale2D(BufferedImage image, int width, int height, Object hint, ColorModel cm) { WritableRaster raster = cm.createCompatibleWritableRaster(width, height); boolean isRasterPremultiplied = cm.isAlphaPremultiplied(); return copyScaled(image, new BufferedImage(cm, raster, isRasterPremultiplied, null), hint); } /** Scales the image using the AWT Image API. */ public static Image scaleAWT(BufferedImage source, int width, int height, int hint) { return source.getScaledInstance(width, height, hint); } /** * Scales the image using the most appropriate API, with the resultant image * having the same color model as the original image. */ public static BufferedImage scale(BufferedImage source, int width, int height, boolean pad) { int w = source.getWidth(); int h = source.getHeight(); if (w == width && h == height) return source; int finalWidth = width, finalHeight = height; if (pad) { // keep aspect ratio the same double r = (double) w / h; double ratio = (double) width / height; if (r > ratio) { // bounded by width; adjust height height = (h * width) / w; } else { // bounded by height; adjust width width = (w * height) / h; } } int pixelType = getPixelType(source); BufferedImage result = null; ColorModel sourceModel = source.getColorModel(); if ((sourceModel instanceof Index16ColorModel) || (sourceModel instanceof IndexColorModel) || (sourceModel instanceof SignedColorModel)) { DataBuffer buffer = source.getData().getDataBuffer(); WritableRaster raster = Raster.createWritableRaster( source.getSampleModel(), buffer, null); ColorModel model = makeColorModel(1, buffer.getDataType()); if (sourceModel instanceof SignedColorModel) { model = sourceModel; } source = new BufferedImage(model, raster, false, null); Image scaled = scaleAWT(source, width, height, Image.SCALE_AREA_AVERAGING); result = makeBuffered(scaled, sourceModel); raster = Raster.createWritableRaster(result.getSampleModel(), result.getData().getDataBuffer(), null); result = new BufferedImage(sourceModel, raster, false, null); } else { if (FormatTools.isSigned(pixelType)) { source = makeUnsigned(source); sourceModel = null; } Image scaled = scaleAWT(source, width, height, Image.SCALE_AREA_AVERAGING); result = makeBuffered(scaled, sourceModel); } return padImage(result, finalWidth, finalHeight); } // -- AWT images -- /** * Creates a buffered image from the given AWT image object. * If the AWT image is already a buffered image, no new object is created. */ public static BufferedImage makeBuffered(Image image) { if (image instanceof BufferedImage) return (BufferedImage) image; // TODO: better way to handle color model (don't just assume RGB) loadImage(image); BufferedImage img = new BufferedImage(image.getWidth(OBS), image.getHeight(OBS), BufferedImage.TYPE_INT_RGB); Graphics g = img.getGraphics(); g.drawImage(image, 0, 0, OBS); g.dispose(); return img; } /** * Creates a buffered image possessing the given color model, * from the specified AWT image object. If the AWT image is already a * buffered image with the given color model, no new object is created. */ public static BufferedImage makeBuffered(Image image, ColorModel cm) { if (cm == null) return makeBuffered(image); if (image instanceof BufferedImage) { BufferedImage bi = (BufferedImage) image; if (cm.equals(bi.getColorModel())) return bi; } loadImage(image); int w = image.getWidth(OBS), h = image.getHeight(OBS); boolean alphaPremultiplied = cm.isAlphaPremultiplied(); WritableRaster raster = cm.createCompatibleWritableRaster(w, h); BufferedImage result = new BufferedImage(cm, raster, alphaPremultiplied, null); Graphics2D g = result.createGraphics(); g.drawImage(image, 0, 0, OBS); g.dispose(); return result; } /** Ensures the given AWT image is fully loaded. */ public static boolean loadImage(Image image) { if (image instanceof BufferedImage) return true; MediaTracker tracker = new MediaTracker(OBS); tracker.addImage(image, 0); try { tracker.waitForID(0); } catch (InterruptedException exc) { return false; } if (MediaTracker.COMPLETE != tracker.statusID(0, false)) return false; return true; } /** * Gets the width and height of the given AWT image, * waiting for it to finish loading if necessary. */ public static Dimension getSize(Image image) { if (image == null) return new Dimension(0, 0); if (image instanceof BufferedImage) { BufferedImage bi = (BufferedImage) image; return new Dimension(bi.getWidth(), bi.getHeight()); } loadImage(image); return new Dimension(image.getWidth(OBS), image.getHeight(OBS)); } // -- Graphics configuration -- /** * Creates a buffered image compatible with the given graphics * configuration, using the given buffered image as a source. * If gc is null, the default graphics configuration is used. */ public static BufferedImage makeCompatible(BufferedImage image, GraphicsConfiguration gc) { if (gc == null) gc = getDefaultConfiguration(); int w = image.getWidth(), h = image.getHeight(); int trans = image.getColorModel().getTransparency(); BufferedImage result = gc.createCompatibleImage(w, h, trans); Graphics2D g2 = result.createGraphics(); g2.drawRenderedImage(image, null); g2.dispose(); return result; } /** Gets the default graphics configuration for the environment. */ public static GraphicsConfiguration getDefaultConfiguration() { GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice gd = ge.getDefaultScreenDevice(); return gd.getDefaultConfiguration(); } // -- Color model -- /** Gets a color space for the given number of color components. */ public static ColorSpace makeColorSpace(int c) { int type; switch (c) { case 1: type = ColorSpace.CS_GRAY; break; case 2: type = TwoChannelColorSpace.CS_2C; break; case 3: type = ColorSpace.CS_sRGB; break; case 4: type = ColorSpace.CS_sRGB; break; default: return null; } return TwoChannelColorSpace.getInstance(type); } /** Gets a color model for the given number of color components. */ public static ColorModel makeColorModel(int c, int dataType) { ColorSpace cs = makeColorSpace(c); return cs == null ? null : new ComponentColorModel(cs, c == 4, false, Transparency.TRANSLUCENT, dataType); } // -- Indexed color conversion -- /** Converts an indexed color BufferedImage to an RGB BufferedImage. */ public static BufferedImage indexedToRGB(BufferedImage img, boolean le) { byte[][] indices = getPixelBytes(img, le); if (indices.length > 1) return img; int pixelType = getPixelType(img); boolean signed = FormatTools.isSigned(pixelType); if (pixelType == FormatTools.UINT8) { IndexColorModel model = (IndexColorModel) img.getColorModel(); byte[][] b = new byte[3][indices[0].length]; for (int i=0; i<indices[0].length; i++) { b[0][i] = (byte) (model.getRed(indices[0][i] & 0xff) & 0xff); b[1][i] = (byte) (model.getGreen(indices[0][i] & 0xff) & 0xff); b[2][i] = (byte) (model.getBlue(indices[0][i] & 0xff) & 0xff); } return makeImage(b, img.getWidth(), img.getHeight(), signed); } else if (pixelType == FormatTools.UINT16) { Index16ColorModel model = (Index16ColorModel) img.getColorModel(); short[][] s = new short[3][indices[0].length / 2]; for (int i=0; i<s[0].length; i++) { int ndx = DataTools.bytesToInt(indices[0], i*2, 2, le) & 0xffff; s[0][i] = (short) (model.getRed(ndx) & 0xffff); s[1][i] = (short) (model.getRed(ndx) & 0xffff); s[2][i] = (short) (model.getRed(ndx) & 0xffff); } return makeImage(s, img.getWidth(), img.getHeight(), signed); } return null; } /** Converts an IndexColorModel to a 2D byte array. */ public static byte[][] get8BitLookupTable(ColorModel model) { if (!(model instanceof IndexColorModel)) return null; IndexColorModel m = (IndexColorModel) model; byte[][] lut = new byte[3][m.getMapSize()]; m.getReds(lut[0]); m.getGreens(lut[1]); m.getBlues(lut[2]); return lut; } /** Convers an Index16ColorModel to a 2D short array. */ public static short[][] getLookupTable(ColorModel model) { if (!(model instanceof Index16ColorModel)) return null; Index16ColorModel m = (Index16ColorModel) model; short[][] lut = new short[3][]; lut[0] = m.getReds(); lut[1] = m.getGreens(); lut[2] = m.getBlues(); return lut; } }