// // ImageTools.java // /* LOCI Bio-Formats package for reading and converting biological file formats. Copyright (C) 2005-@year@ Melissa Linkert, Curtis Rueden, Chris Allan, Eric Kjellman and Brian Loranger. This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library 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 Library General Public License for more details. You should have received a copy of the GNU Library 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; import java.awt.*; import java.awt.color.ColorSpace; import java.awt.geom.AffineTransform; import java.awt.image.*; /** * A utility class with convenience methods for manipulating images. * * Much code was stolen and adapted from DrLaszloJamf's posts at: * http://forum.java.sun.com/thread.jspa?threadID=522483 * * <dl><dt><b>Source code:</b></dt> * <dd><a href="https://skyking.microscopy.wisc.edu/trac/java/browser/trunk/loci/formats/ImageTools.java">Trac</a>, * <a href="https://skyking.microscopy.wisc.edu/svn/java/trunk/loci/formats/ImageTools.java">SVN</a></dd></dl> * * @author Curtis Rueden ctrueden at wisc.edu */ public final class ImageTools { // -- Constants -- /** ImageObserver for working with AWT images. */ protected static final Component OBS = new Container(); // -- Constructor -- private ImageTools() { } // -- Image construction - from 1D (single channel) data arrays -- /** * Creates an image from the given single-channel unsigned byte data. * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. */ public static BufferedImage makeImage(byte[] data, int w, int h) { return makeImage(new byte[][] {data}, w, h); } /** * Creates an image from the given single-channel unsigned short data. * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. */ public static BufferedImage makeImage(short[] data, int w, int h) { return makeImage(new short[][] {data}, w, h); } /** * Creates an image from the given single-channel signed int data. * @param data Array containing image data. * @param w Width of image plane. * @param h Height of image plane. */ public static BufferedImage makeImage(int[] data, int w, int h) { return makeImage(new int[][] {data}, w, h); } /** * 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 unsigned 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. */ public static BufferedImage makeImage(byte[] data, int w, int h, int c, boolean interleaved) { if (c == 1) return makeImage(data, w, h); int dataType = DataBuffer.TYPE_BYTE; DataBuffer buffer = new DataBufferByte(data, c * w * h); return constructImage(c, dataType, w, h, interleaved, false, buffer); } /** * Creates an image from the given unsigned 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. */ public static BufferedImage makeImage(short[] data, int w, int h, int c, boolean interleaved) { if (c == 1) return makeImage(data, w, h); int dataType = DataBuffer.TYPE_USHORT; DataBuffer buffer = new DataBufferUShort(data, c * w * h); return constructImage(c, dataType, w, h, interleaved, false, buffer); } /** * Creates an image from the given signed 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. */ public static BufferedImage makeImage(int[] data, int w, int h, int c, boolean interleaved) { if (c == 1) return makeImage(data, w, h); int dataType = DataBuffer.TYPE_INT; DataBuffer buffer = new DataBufferInt(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 unsigned 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. */ public static BufferedImage makeImage(byte[][] data, int w, int h) { int dataType = DataBuffer.TYPE_BYTE; DataBuffer buffer = new DataBufferByte(data, data[0].length); return constructImage(data.length, dataType, w, h, false, true, buffer); } /** * Creates an image from the given unsigned 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. */ public static BufferedImage makeImage(short[][] data, int w, int h) { int dataType = DataBuffer.TYPE_USHORT; DataBuffer buffer = new DataBufferUShort(data, data[0].length); return constructImage(data.length, dataType, w, h, false, true, buffer); } /** * Creates an image from the given signed 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. */ public static BufferedImage makeImage(int[][] data, int w, int h) { int dataType = DataBuffer.TYPE_INT; DataBuffer buffer = new DataBufferInt(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 data, * performing type conversions as necessary. * @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 little Whether byte array is in little-endian order. */ public static BufferedImage makeImage(byte[] data, int w, int h, int c, boolean interleaved, int bpp, boolean little) { return makeImage(data, w, h, c, interleaved, bpp, false, little); } /** * Creates an image from the given data, * performing type conversions as necessary. * @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. */ public static BufferedImage makeImage(byte[] data, int w, int h, int c, boolean interleaved, int bpp, boolean fp, boolean little) { 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); } else if (pixels instanceof short[]) { return makeImage((short[]) pixels, w, h, c, interleaved); } else if (pixels instanceof int[]) { return makeImage((int[]) pixels, w, h, c, interleaved); } 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 data, * performing type conversions as necessary. * @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 little Whether byte array is in little-endian order. */ public static BufferedImage makeImage(byte[][] data, int w, int h, int bpp, boolean little) { return makeImage(data, w, h, bpp, false, little); } /** * Creates an image from the given data, * performing type conversions as necessary. * @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. */ public static BufferedImage makeImage(byte[][] data, int w, int h, int bpp, boolean fp, boolean little) { 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); else if (v instanceof short[][]) return makeImage((short[][]) v, w, h); else if (v instanceof int[][]) return makeImage((int[][]) v, w, h); else if (v instanceof float[][]) return makeImage((float[][]) v, w, h); else if (v instanceof double[][]) return makeImage((double[][]) v, w, h); return null; } // -- 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>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> * </ul> */ public static BufferedImage blankImage(int w, int h, int c, int type) { switch (type) { case FormatTools.INT8: case FormatTools.UINT8: return makeImage(new byte[c][w * h], w, h); case FormatTools.INT16: case FormatTools.UINT16: return makeImage(new short[c][w * h], w, h); case FormatTools.INT32: case FormatTools.UINT32: return makeImage(new int[c][w * h], w, h); 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. */ private static BufferedImage constructImage(int c, int type, int w, int h, boolean interleaved, boolean banded, DataBuffer buffer) { ColorModel colorModel = makeColorModel(c, type); if (colorModel == null) return null; SampleModel model; 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); return new BufferedImage(colorModel, raster, false, null); } // -- 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) { WritableRaster raster = image.getRaster(); int tt = raster.getTransferType(); if (tt == DataBuffer.TYPE_BYTE) return getBytes(image); else if (tt == DataBuffer.TYPE_USHORT) return getShorts(image); else if (tt == DataBuffer.TYPE_INT) return getInts(image); else if (tt == DataBuffer.TYPE_FLOAT) return getFloats(image); else if (tt == DataBuffer.TYPE_DOUBLE) return getDoubles(image); else return null; } /** Extracts pixel data as arrays of unsigned bytes, one per channel. */ public static byte[][] getBytes(BufferedImage image) { WritableRaster r = image.getRaster(); if (canUseBankDataDirectly(image, 1, DataBuffer.TYPE_BYTE, DataBufferByte.class)) { return ((DataBufferByte) r.getDataBuffer()).getBankData(); } //return getBytes(makeType(image, dataType)); int w = image.getWidth(), h = image.getHeight(), 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(0, 0, 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(); if (canUseBankDataDirectly(image, 2, DataBuffer.TYPE_USHORT, DataBufferUShort.class)) { return ((DataBufferUShort) r.getDataBuffer()).getBankData(); } //return getShorts(makeType(image, DataBuffer.TYPE_USHORT)); int w = image.getWidth(), h = image.getHeight(), 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(0, 0, 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(); if (canUseBankDataDirectly(image, 4, DataBuffer.TYPE_INT, DataBufferInt.class)) { return ((DataBufferInt) r.getDataBuffer()).getBankData(); } // NB: an order of magnitude faster than the naive makeType solution int w = image.getWidth(), h = image.getHeight(), c = r.getNumBands(); int[][] samples = new int[c][w * h]; for (int i=0; i<c; i++) r.getSamples(0, 0, 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(); if (canUseBankDataDirectly(image, 4, DataBuffer.TYPE_FLOAT, DataBufferFloat.class)) { return ((DataBufferFloat) r.getDataBuffer()).getBankData(); } // NB: an order of magnitude faster than the naive makeType solution int w = image.getWidth(), h = image.getHeight(), c = r.getNumBands(); float[][] samples = new float[c][w * h]; for (int i=0; i<c; i++) r.getSamples(0, 0, 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(); if (canUseBankDataDirectly(image, 8, DataBuffer.TYPE_DOUBLE, DataBufferDouble.class)) { return ((DataBufferDouble) r.getDataBuffer()).getBankData(); } // NB: an order of magnitude faster than the naive makeType solution int w = image.getWidth(), h = image.getHeight(), c = r.getNumBands(); double[][] samples = new double[c][w * h]; for (int i=0; i<c; i++) r.getSamples(0, 0, 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(BufferedImage image, int bytesPerPixel, int transferType, Class dataBufferClass) { WritableRaster r = image.getRaster(); 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; } int[] bankIndices = csm.getBankIndices(); 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) { Object pixels = getPixels(img); if (pixels instanceof byte[][]) { byte[][] b = (byte[][]) pixels; return (byte[][]) pixels; } else if (pixels instanceof short[][]) { short[][] s = (short[][]) pixels; byte[][] b = new byte[s.length][s[0].length * 2]; for (int i=0; i<b.length; i++) { for (int j=0; j<s[0].length; j++) { short v = s[i][j]; if (little) { b[i][j*2] = (byte) (v & 0xff); b[i][j*2+1] = (byte) ((v >>> 8) & 0xff); } else { b[i][j*2] = (byte) ((v >>> 8) & 0xff); b[i][j*2+1] = (byte) (v & 0xff); } } } return b; } else if (pixels instanceof int[][]) { int[][] in = (int[][]) pixels; byte[][] b = new byte[in.length][in[0].length * 4]; for (int i=0; i<b.length; i++) { for (int j=0; j<in[0].length; j++) { int v = in[i][j]; if (little) { b[i][j*4] = (byte) (v & 0xff); b[i][j*4+1] = (byte) ((v >> 8) & 0xff); b[i][j*4+2] = (byte) ((v >> 16) & 0xff); b[i][j*4+3] = (byte) ((v >> 24) & 0xff); } else { b[i][j*4] = (byte) ((v >> 24) & 0xff); b[i][j*4+1] = (byte) ((v >> 16) & 0xff); b[i][j*4+2] = (byte) ((v >> 8) & 0xff); b[i][j*4+3] = (byte) (v & 0xff); } } } return b; } else if (pixels instanceof float[][]) { float[][] in = (float[][]) pixels; byte[][] b = new byte[in.length][in[0].length * 4]; for (int i=0; i<b.length; i++) { for (int j=0; j<in[0].length; j++) { int v = Float.floatToIntBits(in[i][j]); if (little) { b[i][j*4] = (byte) (v & 0xff); b[i][j*4+1] = (byte) ((v >> 8) & 0xff); b[i][j*4+2] = (byte) ((v >> 16) & 0xff); b[i][j*4+3] = (byte) ((v >> 24) & 0xff); } else { b[i][j*4] = (byte) ((v >> 24) & 0xff); b[i][j*4+1] = (byte) ((v >> 16) & 0xff); b[i][j*4+2] = (byte) ((v >> 8) & 0xff); b[i][j*4+3] = (byte) (v & 0xff); } } } return b; } else if (pixels instanceof double[][]) { } return null; } /** * 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) { int type = image.getRaster().getDataBuffer().getDataType(); 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: return FormatTools.INT32; case DataBuffer.TYPE_SHORT: return FormatTools.INT16; case DataBuffer.TYPE_USHORT: 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; //} /** Get the bytes from an image, merging the channels as necessary. */ public static byte[] getBytes(BufferedImage img, boolean separated, int c) { byte[][] p = getBytes(img); if (separated || p.length == 1) return p[0]; else { 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; } } /** * Convert an arbitrary primitive type array with 3 samples per pixel to * a 3 x (width * height) byte array. */ public static byte[][] make24Bits(Object pixels, int w, int h, boolean interleaved, boolean reverse) { int[] pix = make24Bits(pixels, w, h, interleaved); byte[][] rtn = new byte[3][pix.length]; for (int i=0; i<pix.length; i++) { byte r = (byte) ((pix[i] >> 16) & 0xff); rtn[1][i] = (byte) ((pix[i] >> 8) & 0xff); byte b = (byte) (pix[i] & 0xff); rtn[0][i] = reverse ? b : r; rtn[2][i] = reverse ? r : b; } return rtn; } /** * Convert an arbitrary primitive type array with 3 samples per pixel to * an int array, i.e. RGB color with 8 bits per pixel. * Does not perform any scaling. */ public static int[] make24Bits(Object pixels, int w, int h, boolean interleaved) { int[] rtn = new int[w * h]; byte[] b = null; // adapted from ImageJ's TypeConverter methods if (pixels instanceof byte[]) b = (byte[]) pixels; else if (pixels instanceof short[]) { short[] s = (short[]) pixels; b = new byte[s.length]; for (int i=0; i<s.length; i++) { int v = s[i] & 0xffff; b[i] = (byte) v; } } else if (pixels instanceof int[]) { int[] s = (int[]) pixels; b = new byte[s.length]; for (int i=0; i<s.length; i++) { int value = s[i] & 0xffffffff; b[i] = (byte) value; } } else if (pixels instanceof float[]) { float[] s = (float[]) pixels; b = new byte[s.length]; for (int i=0; i<s.length; i++) { float value = s[i]; b[i] = (byte) (255 * value); } } else if (pixels instanceof double[]) { double[] s = (double[]) pixels; b = new byte[s.length]; for (int i=0; i<s.length; i++) { double value = s[i]; b[i] = (byte) Math.round(value); } } int c = b.length / rtn.length; for (int i=0; i<rtn.length; i++) { byte[] a = new byte[4]; for (int j=c-1; j>=0; j--) { a[j] = b[interleaved ? i*c + j : i + j*w*h]; } if (c == 1) { for (int j=1; j<a.length; j++) { a[j] = a[0]; } } byte tmp = a[0]; a[0] = a[2]; a[2] = tmp; rtn[i] = DataTools.bytesToInt(a, true); } return rtn; } // -- Image manipulation -- /** * Splits the given multi-channel array into a 2D array. * The "reverse" parameter is false if channels are in RGB order, true if * channels are in BGR order. */ public static byte[][] splitChannels(byte[] array, int c, int bytes, boolean reverse, boolean interleaved) { byte[][] rtn = new byte[c][array.length / c]; if (interleaved) { if (reverse) { int offset = 0; for (int i=c-1; i>=0; i--) { System.arraycopy(array, offset, rtn[i], 0, rtn[i].length); offset += rtn[c].length; } } else { for (int i=0; i<c; i++) { System.arraycopy(array, i * rtn[i].length, rtn[i], 0, rtn[i].length); } } } else { if (reverse) { int next = 0; for (int i=0; i<array.length; i+=c*bytes) { for (int j=c-1; j>=0; j--) { for (int k=0; k<bytes; k++) { if (next < rtn[j].length) { rtn[c - j - 1][next] = array[i + j*bytes + k]; } next++; } next -= bytes; } next += bytes; } } else { int next = 0; for (int i=0; i<array.length; i+=c*bytes) { for (int j=0; j<c; j++) { for (int k=0; k<bytes; k++) { if (next < rtn[j].length) rtn[j][next] = array[i + j*bytes + k]; next++; } next -= bytes; } next += bytes; } } } return rtn; } /** 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); if (o instanceof byte[][]) { byte[][] pix = (byte[][]) o; for (int i=0; i<c; i++) results[i] = makeImage(pix[i], w, h); } else if (o instanceof short[][]) { short[][] pix = (short[][]) o; for (int i=0; i<c; i++) results[i] = makeImage(pix[i], w, h); } else if (o instanceof int[][]) { int[][] pix = (int[][]) o; for (int i=0; i<c; i++) results[i] = makeImage(pix[i], w, h); } 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(); 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); } if (type == DataBuffer.TYPE_USHORT) { 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); } 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); } 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); } boolean needsPadding = img.getWidth() != width || img.getHeight() != height; if (needsPadding) { Object pixels = getPixels(img); 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] = padImage(b[i], false, 1, img.getWidth(), width, height); } return makeImage(newBytes, width, height); } 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] = padImage(b[i], false, 1, img.getWidth(), width, height); } return makeImage(newShorts, width, height); } 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] = padImage(b[i], false, 1, img.getWidth(), width, height); } return makeImage(newInts, width, height); } 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] = 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] = padImage(b[i], false, 1, img.getWidth(), width, height); } return makeImage(newDoubles, width, height); } return null; } return img; } /** * Pads (or crops) the byte array to the given width and height. * The image will be centered within the new bounds. */ public static byte[] padImage(byte[] b, boolean interleaved, int c, int oldWidth, int width, int height) { int oldHeight = b.length / (oldWidth * c); byte[] padded = new byte[height * width * c]; int wClip = (width - oldWidth) / 2; int hClip = (height - oldHeight) / 2; int h = height < oldHeight ? height : oldHeight; if (interleaved) { int len = oldWidth < width ? oldWidth : width; if (h == oldHeight) { for (int y=0; y<h*c; y++) { int oldIndex = oldWidth * y; int index = width * y; System.arraycopy(b, oldIndex, padded, index, len); } } else { for (int ch=0; ch<c; ch++) { for (int y=0; y<h; y++) { int oldIndex = oldWidth * ch * oldHeight + oldWidth * y; int index = width * ch * height + width * y; System.arraycopy(b, oldIndex, padded, index, len); } } } } else { int len = oldWidth < width ? oldWidth * c : width * c; for (int oy=0, y=0; oy<oldHeight; oy++, y++) { int oldIndex = oldWidth * c * y; int index = width * c * (y + hClip) + c * wClip; System.arraycopy(b, oldIndex, padded, index, len); } } return padded; } /** * Pads (or crops) the short array to the given width and height. * The image will be centered within the new bounds. */ public static short[] padImage(short[] b, boolean interleaved, int c, int oldWidth, int width, int height) { int oldHeight = b.length / (oldWidth * c); short[] padded = new short[height * width * c]; int wClip = (width - oldWidth) / 2; int hClip = (height - oldHeight) / 2; int h = height < oldHeight ? height : oldHeight; if (interleaved) { int len = oldWidth < width ? oldWidth : width; if (h == oldHeight) { for (int y=0; y<h*c; y++) { int oldIndex = oldWidth * y; int index = width * y; System.arraycopy(b, oldIndex, padded, index, len); } } else { for (int ch=0; ch<c; ch++) { for (int y=0; y<h; y++) { int oldIndex = oldWidth * ch * oldHeight + oldWidth * y; int index = width * ch * height + width * y; System.arraycopy(b, oldIndex, padded, index, len); } } } } else { int len = oldWidth < width ? oldWidth * c : width * c; for (int oy=0, y=0; oy<oldHeight; oy++, y++) { int oldIndex = oldWidth * c * y; int index = width * c * (y + hClip) + c * wClip; System.arraycopy(b, oldIndex, padded, index, len); } } return padded; } /** * Pads (or crops) the int array to the given width and height. * The image will be centered within the new bounds. */ public static int[] padImage(int[] b, boolean interleaved, int c, int oldWidth, int width, int height) { int oldHeight = b.length / (oldWidth * c); int[] padded = new int[height * width * c]; int wClip = (width - oldWidth) / 2; int hClip = (height - oldHeight) / 2; int h = height < oldHeight ? height : oldHeight; if (interleaved) { int len = oldWidth < width ? oldWidth : width; if (h == oldHeight) { for (int y=0; y<h*c; y++) { int oldIndex = oldWidth * y; int index = width * y; System.arraycopy(b, oldIndex, padded, index, len); } } else { for (int ch=0; ch<c; ch++) { for (int y=0; y<h; y++) { int oldIndex = oldWidth * ch * oldHeight + oldWidth * y; int index = width * ch * height + width * y; System.arraycopy(b, oldIndex, padded, index, len); } } } } else { int len = oldWidth < width ? oldWidth * c : width * c; for (int oy=0, y=0; oy<oldHeight; oy++, y++) { int oldIndex = oldWidth * c * y; int index = width * c * (y + hClip) + c * wClip; System.arraycopy(b, oldIndex, padded, index, len); } } return padded; } /** * Pads (or crops) the float array to the given width and height. * The image will be centered within the new bounds. */ public static float[] padImage(float[] b, boolean interleaved, int c, int oldWidth, int width, int height) { int oldHeight = b.length / (oldWidth * c); float[] padded = new float[height * width * c]; int wClip = (width - oldWidth) / 2; int hClip = (height - oldHeight) / 2; int h = height < oldHeight ? height : oldHeight; if (interleaved) { int len = oldWidth < width ? oldWidth : width; if (h == oldHeight) { for (int y=0; y<h*c; y++) { int oldIndex = oldWidth * y; int index = width * y; System.arraycopy(b, oldIndex, padded, index, len); } } else { for (int ch=0; ch<c; ch++) { for (int y=0; y<h; y++) { int oldIndex = oldWidth * ch * oldHeight + oldWidth * y; int index = width * ch * height + width * y; System.arraycopy(b, oldIndex, padded, index, len); } } } } else { int len = oldWidth < width ? oldWidth * c : width * c; for (int oy=0, y=0; oy<oldHeight; oy++, y++) { int oldIndex = oldWidth * c * y; int index = width * c * (y + hClip) + c * wClip; System.arraycopy(b, oldIndex, padded, index, len); } } return padded; } /** * Pads (or crops) the double array to the given width and height. * The image will be centered within the new bounds. */ public static double[] padImage(double[] b, boolean interleaved, int c, int oldWidth, int width, int height) { int oldHeight = b.length / (oldWidth * c); double[] padded = new double[height * width * c]; int wClip = (width - oldWidth) / 2; int hClip = (height - oldHeight) / 2; int h = height < oldHeight ? height : oldHeight; if (interleaved) { int len = oldWidth < width ? oldWidth : width; if (h == oldHeight) { for (int y=0; y<h*c; y++) { int oldIndex = oldWidth * y; int index = width * y; System.arraycopy(b, oldIndex, padded, index, len); } } else { for (int ch=0; ch<c; ch++) { for (int y=0; y<h; y++) { int oldIndex = oldWidth * ch * oldHeight + oldWidth * y; int index = width * ch * height + width * y; System.arraycopy(b, oldIndex, padded, index, len); } } } } else { int len = oldWidth < width ? oldWidth * c : width * c; for (int oy=0, y=0; oy<oldHeight; oy++, y++) { int oldIndex = oldWidth * c * y; int index = width * c * (y + hClip) + c * wClip; System.arraycopy(b, oldIndex, padded, index, len); } } return padded; } /** Perform demosaicing on a byte array, assuming a {B, G, G, R} mosaic. */ public static short[][] demosaic(short[][] input, int w, int h) { for (int i=0; i<input[0].length; i++) { // determine which color components need to be calculated boolean needsRed = !(((i / w) % 2 == 1) && ((i % w) % 2 == 1)); boolean needsBlue = !(((i / w) % 2 == 0) && ((i % w) % 2 == 0)); boolean needsGreen = needsBlue ^ needsRed; if (needsRed) { int sum = 0; int count = 0; int[] indices = null; if (!needsBlue) { indices = new int[] {i - w - 1, i - w + 1, i + w - 1, i + w + 1}; } else if ((i / w) % 2 == 1) indices = new int[] {i - 1, i + 1}; else indices = new int[] {i - w, i + w}; for (int j=0; j<indices.length; j++) { if (indices[j] < input[0].length && indices[j] >= 0) { sum += (int) input[0][indices[j]]; count++; } } if (count > 0) { input[0][i] = (short) (sum / count); } } if (needsGreen) { int sum = 0; int count = 0; int[] indices = {i - w, i - 1, i + 1, i + w}; for (int j=0; j<indices.length; j++) { if (indices[j] < input[0].length && indices[j] >= 0) { sum += (int) input[1][indices[j]]; count++; } } if (count > 0) { input[1][i] = (short) (sum / count); } } if (needsBlue) { int sum = 0; int count = 0; int[] indices = null; if (!needsRed) { indices = new int[] {i - w - 1, i - w + 1, i + w - 1, i + w + 1}; } else if ((i / w) % 2 == 1) indices = new int[] {i - w, i + w}; else indices = new int[] {i - 1, i + 1}; for (int j=0; j<indices.length; j++) { if (indices[j] < input[0].length && indices[j] >= 0) { sum += (int) input[2][indices[j]]; count++; } } if (count > 0) { input[2][i] = (short) (sum / count); } } } // Before returning, perform convolution with a 5x5 pseudo-Gaussian filter. // This is *not* an optimal filter, but it works reasonably well. short[] newRed = new short[w * h]; short[] newGreen = new short[w * h]; short[] newBlue = new short[w * h]; float[][] kernel = new float[5][5]; kernel[0] = new float[] {0.458f, 0.823f, 1f, 0.823f, 0.458f}; kernel[1] = new float[] {0.823f, 1f, 1.09f, 1f, 0.823f}; kernel[2] = new float[] {1f, 1.09f, 1.135f, 1.09f, 1f}; kernel[3] = new float[] {0.823f, 1f, 1.09f, 1f, 0.823f}; kernel[4] = new float[] {0.458f, 0.823f, 1f, 0.823f, 0.458f}; for (int i=0; i<h; i++) { for (int j=0; j<w; j++) { float redRow = 0, greenRow = 0, blueRow = 0; float[] kernelRow = kernel[i % 5]; int diff = w - j; float sum = 0; if (diff < 5) { while (diff > 0) { int off = i*w + j + diff - 1; redRow += kernelRow[diff - 1] * input[0][off]; greenRow += kernelRow[diff - 1] * input[1][off]; blueRow += kernelRow[diff - 1] * input[2][off]; sum += kernelRow[diff - 1]; diff--; } } else { for (int m=0; m<5; m++) { int off = i*w + j + m; redRow += kernelRow[m] * input[0][off]; greenRow += kernelRow[m] * input[1][off]; blueRow += kernelRow[m] * input[2][off]; sum += kernelRow[m]; } } if (sum == 0) sum = 1; newRed[i*w + j] = (short) (redRow / sum); newGreen[i*w + j] = (short) (greenRow / sum); newBlue[i*w + j] = (short) (blueRow / sum); } } for (int i=0; i<h; i++) { for (int j=0; j<w; j++) { float redCol = 0, greenCol = 0, blueCol = 0; float[] kernelCol = kernel[j % 5]; int diff = h - i; float sum = 0; if (diff < 5) { while (diff > 0) { int off = (i + diff - 1) * w + j; redCol += kernelCol[diff - 1] * newRed[off]; greenCol += kernelCol[diff - 1] * newGreen[off]; blueCol += kernelCol[diff - 1] * newBlue[off]; sum += kernelCol[diff - 1]; diff--; } } else { for (int m=0; m<5; m++) { int off = (i + m) * w + j; redCol += kernelCol[m] * newRed[off]; greenCol += kernelCol[m] * newGreen[off]; blueCol += kernelCol[m] * newBlue[off]; sum += kernelCol[m]; } } if (sum == 0) sum = 1; input[0][i*w + j] = (short) (redCol / sum); input[1][i*w + j] = (short) (greenCol / sum); input[2][i*w + j] = (short) (blueCol / sum); } } return input; } /** * 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); 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; float diff = (float) max - (float) 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()); } 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()); } 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()); } 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()); } return img; } /** * Perform autoscaling on the given byte array; * map min to 0 and max to 255. If the number of bytes per pixel is 1, then * nothing happens. */ public static byte[] autoscale(byte[] b, int min, int max, int bpp, boolean little) { if (bpp == 1) return b; byte[] out = new byte[b.length / bpp]; for (int i=0; i<b.length; i+=bpp) { int s = DataTools.bytesToInt(b, i, bpp, little); if (s >= max) s = 255; else if (s <= min) s = 0; else { int diff = max - min; float dist = (s - min) / diff; s = (int) dist * 256; } out[i / bpp] = (byte) s; } return out; } /** Scan a plane for the channel min and max values. */ public static Double[] scanData(byte[] plane, int bits, boolean littleEndian) { int max = 0; int min = Integer.MAX_VALUE; if (bits <= 8) { for (int j=0; j<plane.length; j++) { if (plane[j] < min) min = plane[j]; if (plane[j] > max) max = plane[j]; } } else if (bits == 16) { for (int j=0; j<plane.length; j+=2) { short s = DataTools.bytesToShort(plane, j, 2, littleEndian); if (s < min) min = s; if (s > max) max = s; } } else if (bits == 32) { for (int j=0; j<plane.length; j+=4) { int s = DataTools.bytesToInt(plane, j, 4, littleEndian); if (s < min) min = s; if (s > max) max = s; } } Double[] rtn = new Double[2]; rtn[0] = new Double(min); rtn[1] = new Double(max); return rtn; } // -- 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; } } BufferedImage result = null; Image scaled = scaleAWT(source, width, height, Image.SCALE_AREA_AVERAGING); result = makeBuffered(scaled, source.getColorModel()); 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 (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 ColorModel makeColorModel(int c, int dataType) { 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 new ComponentColorModel(TwoChannelColorSpace.getInstance(type), c == 4, false, ColorModel.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; if (getPixelType(img) == FormatTools.UINT8) { IndexedColorModel model = (IndexedColorModel) 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()); } else if (getPixelType(img) == FormatTools.UINT16) { IndexedColorModel model = (IndexedColorModel) 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()); } return null; } /** Converts a LUT and an array of indices into an array of RGB tuples. */ public static byte[][] indexedToRGB(byte[][] lut, byte[] b) { byte[][] rtn = new byte[lut.length][b.length]; for (int i=0; i<b.length; i++) { for (int j=0; j<lut.length; j++) { rtn[j][i] = lut[j][b[i]]; } } return rtn; } /** Converts a LUT and an array of indices into an array of RGB tuples. */ public static short[][] indexedToRGB(short[][] lut, byte[] b, boolean le) { short[][] rtn = new short[lut.length][b.length / 2]; for (int i=0; i<b.length/2; i++) { for (int j=0; j<lut.length; j++) { rtn[j][i] = lut[j][DataTools.bytesToShort(b, i*2, 2, le)]; } } return rtn; } }