// // ImageFlatField.java // /* VisAD system for interactive analysis and visualization of numerical data. Copyright (C) 1996 - 2017 Bill Hibbard, Curtis Rueden, Tom Rink, Dave Glowacki, Steve Emmerson, Tom Whittaker, Don Murray, and Tommy Jasmin. This library 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 library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA */ package visad; import java.awt.Graphics2D; import java.awt.color.ColorSpace; import java.awt.image.*; import java.rmi.RemoteException; import java.util.Arrays; /** * ImageFlatField is a VisAD FlatField backed by a java.awt.image.BufferedImage * object, instead of the usual float[][] or double[][] samples array. * Expands the samples into floats or doubles on demand using * {@link #unpackFloats(boolean)}, which can be expensive for repeated * operations of certain types. Such calls can be avoided for certain types of * visualization using an 8-bit image with * {@link visad.bom.ShadowImageFunctionTypeJ3D}. */ public class ImageFlatField extends FlatField { // -- Constants -- /** Debugging flag. */ public static final boolean DEBUG = false; // -- Fields -- /** The image backing this FlatField. */ protected BufferedImage image; /** Dimensions of the image. */ protected int num, width, height; // -- Static methods -- /** * Converts the given BufferedImage to a format efficient * with ImageFlatField's grabBytes method and usable with * Java3D texturing by reference. */ public static BufferedImage make3ByteRGB(BufferedImage image) { // create BufferedImage of "TYPE_3BYTE_RGB" (TYPE_CUSTOM) // This type of image is efficient with grabBytes, and is // compatible with Java3D's texturing by reference support. if (image == null) return null; int dataType = DataBuffer.TYPE_BYTE; ColorModel colorModel = new ComponentColorModel( ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, ColorModel.TRANSLUCENT, dataType); int w = image.getWidth(), h = image.getHeight(); byte[][] data = new byte[3][w * h]; SampleModel model = new BandedSampleModel(dataType, w, h, data.length); DataBuffer buffer = new DataBufferByte(data, data[0].length); WritableRaster raster = Raster.createWritableRaster(model, buffer, null); BufferedImage result = new BufferedImage(colorModel, raster, false, null); // paint image into buffered image Graphics2D g = result.createGraphics(); g.drawRenderedImage(image, null); g.dispose(); g = null; return result; } /** Constructs a FunctionType suitable for use with the given image. */ public static FunctionType makeFunctionType(BufferedImage img) throws VisADException { if (img == null) throw new VisADException("image cannot be null"); RealType x = RealType.getRealType("ImageElement"); RealType y = RealType.getRealType("ImageLine"); RealTupleType xy = new RealTupleType(x, y); int num = img.getRaster().getNumBands(); MathType range = null; if (num == 4) { RealType r = RealType.getRealType("Red"); RealType g = RealType.getRealType("Green"); RealType b = RealType.getRealType("Blue"); RealType a = RealType.getRealType("Alpha"); range = new RealTupleType(r, g, b, a); } else if (num == 3) { RealType r = RealType.getRealType("Red"); RealType g = RealType.getRealType("Green"); RealType b = RealType.getRealType("Blue"); range = new RealTupleType(r, g, b); } else if (num == 1) range = RealType.getRealType("Intensity"); else throw new VisADException("Unsupported # of bands (" + num + ")"); return new FunctionType(xy, range); } /** Constructs a domain Set suitable for use with the given image. */ public static Set makeDomainSet(BufferedImage img) throws VisADException { RealType x = RealType.getRealType("ImageElement"); RealType y = RealType.getRealType("ImageLine"); RealTupleType xy = new RealTupleType(x, y); int w = img.getWidth(), h = img.getHeight(); return new Linear2DSet(xy, 0, w - 1, w, h - 1, 0, h); } // -- Constructors -- /** Constructs an ImageFlatField around the given BufferedImage. */ public ImageFlatField(BufferedImage img) throws VisADException, RemoteException { this(makeFunctionType(img), makeDomainSet(img)); setImage(img); } public ImageFlatField(FunctionType type) throws VisADException { this(type, type.getDomain().getDefaultSet(), null, null, null, null); } public ImageFlatField(FunctionType type, Set domain_set) throws VisADException { this(type, domain_set, null, null, null, null); } public ImageFlatField(FunctionType type, Set domain_set, CoordinateSystem range_coord_sys, Set[] range_sets, Unit[] units) throws VisADException { this(type, domain_set, range_coord_sys, null, range_sets, units); } public ImageFlatField(FunctionType type, Set domain_set, CoordinateSystem[] range_coord_syses, Set[] range_sets, Unit[] units) throws VisADException { this(type, domain_set, null, range_coord_syses, range_sets, units); } public ImageFlatField(FunctionType type, Set domain_set, CoordinateSystem range_coord_sys, CoordinateSystem[] range_coord_syses, Set[] range_sets, Unit[] units) throws VisADException { super(type, domain_set, range_coord_sys, range_coord_syses, range_sets, units); RealTupleType domain = type.getDomain(); if (domain.getNumberOfRealComponents() != 2) { throw new VisADException( "FunctionType domain must be flat with 2 components"); } MathType range = type.getRange(); if (range instanceof RealType) num = 1; else if (range instanceof RealTupleType) { num = ((RealTupleType) range).getNumberOfRealComponents(); } if (num != 1 && num != 3 && num != 4) { throw new VisADException( "FunctionType range must be flat with 1, 3 or 4 components"); } if (domain_set instanceof Gridded2DSet) { int[] len = ((Gridded2DSet) domain_set).getLengths(); width = len[0]; height = len[1]; } else { throw new VisADException( "Domain set must be Gridded2DSet"); } } // -- ImageFlatField API methods -- /** Gets the image backing this FlatField. */ public BufferedImage getImage() { pr ("getImage"); return image; } /** Sets the image backing this FlatField. */ public void setImage(BufferedImage image) throws VisADException, RemoteException { // pr ("setImage"); if (image == null) throw new VisADException("image cannot be null"); if (image.getWidth() != width || image.getHeight() != height) { throw new VisADException("Image dimensions do not match domain set"); } if (image.getRaster().getNumBands() != num) { throw new VisADException( "Image component count does not match FunctionType range"); } this.image = image; clearMissing(); notifyReferences(); } /** Gets RealType for each domain component (X and Y). */ public RealType[] getDomainTypes() { RealTupleType domain = ((FunctionType) getType()).getDomain(); return domain.getRealComponents(); } /** Gets RealType for each range component. */ public RealType[] getRangeTypes() { MathType range = ((FunctionType) getType()).getRange(); RealType[] v = null; if (range instanceof RealType) v = new RealType[] {(RealType) range}; else if (range instanceof TupleType) { v = ((TupleType) range).getRealComponents(); } else v = new RealType[0]; return v; } // -- FlatField API methods -- public void setSamples(Data[] range, boolean copy) throws VisADException, RemoteException { throw new VisADException("Use setImage(Image) for ImageFlatField"); } /** * This method has been overridden to avoid a call to * unpackValues or unpackFloats during range computation. */ public DataShadow computeRanges(ShadowType type, DataShadow shadow) throws VisADException { if (isMissing()) return shadow; ShadowRealTupleType domainType = ((ShadowFunctionType) type).getDomain(); int n = domainType.getDimension(); double[][] ranges = new double[2][n]; // DomainSet.computeRanges handles Reference shadow = getDomainSet().computeRanges(domainType, shadow, ranges, true); ShadowRealTupleType shadRef; // skip range if no range components are mapped int[] indices = ((ShadowFunctionType) type).getRangeDisplayIndices(); boolean anyMapped = false; for (int i=0; i<TupleDimension; i++) { if (indices[i] >= 0) anyMapped = true; } if (!anyMapped) return shadow; // check for any range coordinate systems boolean anyRangeRef = (RangeCoordinateSystem != null); if (RangeCoordinateSystems != null) { for (int i=0; i<RangeCoordinateSystems.length; i++) { anyRangeRef |= (RangeCoordinateSystems[i] != null); } } ranges = anyRangeRef ? new double[2][TupleDimension] : null; // get image raster WritableRaster raster = image.getRaster(); double[] min = new double[TupleDimension]; Arrays.fill(min, Double.MAX_VALUE); double[] max = new double[TupleDimension]; Arrays.fill(max, -Double.MAX_VALUE); double[] vals = new double[TupleDimension]; for (int y=0; y<height; y++) { for (int x=0; x<width; x++) { raster.getPixel(x, y, vals); for (int i=0; i<TupleDimension; i++) { min[i] = Math.min(min[i], vals[i]); max[i] = Math.max(max[i], vals[i]); } } } for (int i=0; i<TupleDimension; i++) { int k = indices[i]; if (k >= 0 || anyRangeRef) { Unit dunit = ((RealType) ((FunctionType) Type).getFlatRange().getComponent(i)).getDefaultUnit(); if (dunit != null && !dunit.equals(RangeUnits[i])) { min[i] = dunit.toThis(min[i], RangeUnits[i]); max[i] = dunit.toThis(max[i], RangeUnits[i]); } if (anyRangeRef) { ranges[0][i] = Math.min(ranges[0][i], min[i]); ranges[1][i] = Math.max(ranges[1][i], max[i]); } if (k >= 0 && k < shadow.ranges[0].length) { shadow.ranges[0][k] = Math.min(shadow.ranges[0][k], min[i]); shadow.ranges[1][k] = Math.max(shadow.ranges[1][k], max[i]); } } } if (RangeCoordinateSystem != null) { // computeRanges for Reference (relative to range) RealTypes ShadowRealTupleType rangeType = (ShadowRealTupleType) ((ShadowFunctionType) type).getRange(); shadRef = rangeType.getReference(); shadow = computeReferenceRanges(rangeType, RangeCoordinateSystem, RangeUnits, shadow, shadRef, ranges); } else if (RangeCoordinateSystems != null) { TupleType rangeTupleType = (TupleType) ((FunctionType) Type).getRange(); int j = 0; for (int i=0; i<RangeCoordinateSystems.length; i++) { MathType component = rangeTupleType.getComponent(i); if (component instanceof RealType) j++; else { // (component instanceof RealTupleType) int m = ((RealTupleType) component).getDimension(); if (RangeCoordinateSystems[i] != null) { // computeRanges for Reference (relative to range // component) RealTypes double[][] subRanges = new double[2][m]; Unit[] subUnits = new Unit[m]; for (int k=0; k<m; k++) { subRanges[0][k] = ranges[0][j]; subRanges[1][k] = ranges[1][j]; subUnits[k] = RangeUnits[j]; j++; } ShadowRealTupleType rangeType = (ShadowRealTupleType) ((ShadowTupleType) ((ShadowFunctionType) type).getRange()).getComponent(i); shadRef = rangeType.getReference(); shadow = computeReferenceRanges(rangeType, RangeCoordinateSystems[i], subUnits, shadow, shadRef, subRanges); } else { // (RangeCoordinateSystems[i] == null) j += m; } } // end if (component instanceof RealTupleType) } // end for (int i=0; i<RangeCoordinateSystems.length; i++) } // end if (RangeCoordinateSystems != null) return shadow; } /** * Unpacks an array of doubles from field sample values. * * @param copy Ignored (always returns a copy). */ protected double[][] unpackValues(boolean copy) throws VisADException { pr ("unpackValues(" + copy + ")"); // copy flag is ignored Raster r = image.getRaster(); double[][] samps = new double[num][width * height]; for (int c=0; c<num; c++) r.getSamples(0, 0, width, height, c, samps[c]); return samps; } /** * Unpacks an array of floats from field sample values. * * @param copy Ignored (always returns a copy). */ protected float[][] unpackFloats(boolean copy) throws VisADException { pr ("unpackFloats(" + copy + ")"); // copy flag is ignored Raster r = image.getRaster(); float[][] samps = new float[num][width * height]; for (int c=0; c<num; c++) r.getSamples(0, 0, width, height, c, samps[c]); return samps; } protected double[] unpackValues(int s_index) throws VisADException { pr ("unpackValues(" + s_index + ")"); Raster r = image.getRaster(); double[] samps = new double[num]; r.getPixel(s_index % width, s_index / width, samps); return samps; } protected float[] unpackFloats(int s_index) throws VisADException { pr ("unpackFloats(" + s_index + ")"); Raster r = image.getRaster(); float[] samps = new float[num]; r.getPixel(s_index % width, s_index / width, samps); return samps; } protected double[] unpackOneRangeComp(int comp) throws VisADException { pr ("unpackOneRangeComp(" + comp + ")"); Raster r = image.getRaster(); double[] samps = new double[width * height]; r.getSamples(0, 0, width, height, comp, samps); return samps; } public Data getSample(int index) throws VisADException, RemoteException { double[] v = unpackValues(index); RealTupleType range = (RealTupleType) ((FunctionType) getType()).getRange(); return new RealTuple(range, v); } protected void pr(String message) { if (DEBUG) { String s = hashCode () + " " + getClass().getName () + " " + message; new Exception(s).printStackTrace(); } } // -- FlatFieldIface API methods -- public void setSamples(double[][] range, boolean copy) throws RemoteException, VisADException { throw new VisADException("Use setImage(Image) for ImageFlatField"); } public void setSamples(float[][] range, boolean copy) throws RemoteException, VisADException { throw new VisADException("Use setImage(Image) for ImageFlatField"); } public void setSamples(double[][] range, ErrorEstimate[] errors, boolean copy) throws RemoteException, VisADException { throw new VisADException("Use setImage(Image) for ImageFlatField"); } public void setSamples(int start, double[][] range) throws RemoteException, VisADException { throw new VisADException("Use setImage(Image) for ImageFlatField"); } public void setSamples(float[][] range, ErrorEstimate[] errors, boolean copy) throws RemoteException, VisADException { throw new VisADException("Use setImage(Image) for ImageFlatField"); } public byte[][] grabBytes() { pr ("grabBytes"); byte[][] data = grabBytes(image); if (data == null) return null; if (data.length > num) { byte[][] bytes = new byte[num][]; System.arraycopy(data, 0, bytes, 0, num); data = bytes; } return data; } public static byte[][] grabBytes(BufferedImage image) { WritableRaster raster = image.getRaster(); if (raster.getTransferType() != DataBuffer.TYPE_BYTE) return null; DataBuffer buffer = raster.getDataBuffer(); if (buffer instanceof DataBufferByte) { SampleModel model = raster.getSampleModel(); if (model instanceof BandedSampleModel) { // fastest way to extract bytes; no copy if (DEBUG) System.err.println("grabBytes: FAST"); byte[][] data = ((DataBufferByte) buffer).getBankData(); return data; } else if (model instanceof ComponentSampleModel) { // medium speed way to extract bytes; direct array copy if (DEBUG) System.err.println("grabBytes: MEDIUM"); byte[][] data = ((DataBufferByte) buffer).getBankData(); ComponentSampleModel csm = (ComponentSampleModel) model; int[] bandOffsets = csm.getBandOffsets(); int[] bankIndices = csm.getBankIndices(); int pixelStride = csm.getPixelStride(); int scanlineStride = csm.getScanlineStride(); int numBands = bandOffsets.length; int width = image.getWidth(); int height = image.getHeight(); int numPixels = width * height; byte[][] bytes = new byte[numBands][numPixels]; for (int c=0; c<numBands; c++) { for (int h=0; h<height; h++) { for (int w=0; w<width; w++) { int ndx = width * h + w; int q = bandOffsets[c] + h * scanlineStride + w * pixelStride; bytes[c][ndx] = data[bankIndices[c]][q]; } } } return bytes; } // model instanceof ComponentSampleModel } // buffer instanceof DataBufferByte if (DEBUG) System.err.println("grabBytes: make3ByteRGB"); return grabBytes(make3ByteRGB(image)); /* // slower, more general way to extract bytes; use PixelGrabber // CTR NOTE - Something is fishy with this method of pixel extraction. // Results do not seem to match those of the FAST or MEDIUM methods. if (DEBUG) System.err.println("grabBytes: SLOW"); int numPixels = width * height; int[] words = new int[numPixels]; PixelGrabber grabber = new PixelGrabber( image.getSource(), 0, 0, width, height, words, 0, width); try { grabber.grabPixels(); } catch (InterruptedException e) { e.printStackTrace(); } ColorModel cm = grabber.getColorModel(); byte[][] bytes = new byte[num][numPixels]; for (int i=0; i<numPixels; i++) { int pixel = words[i]; int a = (pixel >> 24) & 0xff; int r = (pixel >> 16) & 0xff; int g = (pixel >> 8) & 0xff; int b = pixel & 0xff; bytes[0][i] = (byte) r; if (num >= 2) bytes[1][i] = (byte) g; if (num >= 3) bytes[2][i] = (byte) b; if (num >= 4) bytes[3][i] = (byte) a; } return bytes; */ } }