/* Copyright (C) 2001, 2007 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All Rights Reserved. */ package gov.nasa.worldwind.formats.wvt; import gov.nasa.worldwind.util.*; import java.awt.*; import java.awt.image.*; import java.io.*; /** * @author brownrigg * @version $Id$ */ public class WaveletCodec { private final int type; private final int resolutionX; private final int resolutionY; private byte[][] xform; public static final int TYPE_BYTE_GRAY = 0x67726179; // ascii "gray" public static final int TYPE_3BYTE_BGR = 0x72676220; // ascii "rgb " public static final int TYPE_4BYTE_ARGB = 0x61726762; // ascii "argb" /** * A suggested filename extension for wavelet-encodings. */ public static final String WVT_EXT = ".wvt"; private WaveletCodec(int type, int resolutionX, int resolutionY) { if (!isTypeValid(type)) { String message = "Invalid type: " + type; Logging.logger().severe(message); throw new IllegalArgumentException(message); } this.type = type; this.resolutionX = resolutionX; this.resolutionY = resolutionY; } public final int getType() { return type; } /** * Returns the resolution of this wavelet encoding. * * @return resolution */ public final int getResolutionX() { return this.resolutionX; } /** * Returns the resolution of this wavelet encoding. * * @return resolution */ public final int getResolutionY() { return this.resolutionY; } /** * Reconstructs an image from this wavelet encoding at the given resolution. The specified resolution * must be a power of two, and must be less than or equal to the resolution of the encoding. * * This reconstruction algorithm was hinted at in: * * "Principles of Digital Image Synthesis" * Andrew Glassner * 1995, pp. 296 * * @param resolution * @return reconstructed image. * @throws IllegalArgumentException */ public BufferedImage reconstruct(int resolution) throws IllegalArgumentException { // Allocate memory for the BufferedImage int numBands = this.xform.length; int[][] imageData = new int[numBands][this.resolutionX * this.resolutionY]; byte[][] imageBytes = new byte[numBands][this.resolutionX * this.resolutionY]; // we need working buffers as large as 1/2 the output resolution... // Note how these are named after Glassner's convention... int res2 = (resolution/2) * (resolution/2); int[][] A = new int[numBands][res2]; int[][] D = new int[numBands][res2]; int[][] V = new int[numBands][res2]; int[][] H = new int[numBands][res2]; // Prime the process. Recall that the first byte of each channel is a color value, not // signed coefficients. So treat it as an unsigned value. for (int k=0; k < numBands; k++) imageData[k][0] = 0x000000ff & this.xform[k][0]; int scale = 1; int offset = 1; do { // load up our A,D,V,H component arrays... int numVals = scale*scale; if (numVals >= resolution*resolution) break; int next = 0; for (int j=0; j<scale; j++) { for (int i=0; i<scale; i++, next++) { for (int k=0; k<numBands; k++) { A[k][next] = imageData[k][j*resolution + i]; } } } for (int i=0; i<numVals; i++, offset++) { for (int k=0; k<numBands; k++) { H[k][i] = this.xform[k][offset]; } } for (int i=0; i<numVals; i++, offset++) { for (int k=0; k<numBands; k++) { V[k][i] = this.xform[k][offset]; } } for (int i=0; i<numVals; i++, offset++) { for (int k=0; k<numBands; k++) { D[k][i] = this.xform[k][offset]; } } next = 0; for (int j = 0; j < scale; j++) { for (int i = 0; i < scale; i++, next++) { for (int k = 0; k < numBands; k++) { int a = A[k][next] + H[k][next] + V[k][next] + D[k][next]; int b = A[k][next] - H[k][next] + V[k][next] - D[k][next]; int c = A[k][next] + H[k][next] - V[k][next] - D[k][next]; int d = A[k][next] - H[k][next] - V[k][next] + D[k][next]; imageData[k][2*j*resolution + (i*2)] = a; imageData[k][2*j*resolution + (i*2) + 1] = b; imageData[k][2*j*resolution + resolution + (i*2)] = c; imageData[k][2*j*resolution + resolution + (i*2) + 1] = d; } } } scale *= 2; } while (scale < resolution); // Copy to bytes and clamp to byte-range... for (int j = 0; j < resolution; j++) { for (int i = 0; i < resolution; i++) { for (int k = 0; k < numBands; k++) { imageBytes[k][j*resolution+i] = (byte) Math.max(0, Math.min(255, imageData[k][j*resolution+i])); } } } // Finally, construct a BufferedImage... BandedSampleModel sm = new BandedSampleModel(DataBuffer.TYPE_BYTE, resolution, resolution, numBands); DataBufferByte dataBuff = new DataBufferByte(imageBytes, imageBytes[0].length); WritableRaster rast = Raster.createWritableRaster(sm, dataBuff, new Point(0, 0)); int imageType = getBufferedImageType(this); BufferedImage image = new BufferedImage(resolution, resolution, imageType); image.getRaster().setRect(rast); return image; } public static java.nio.ByteBuffer save(WaveletCodec codec) throws IOException { if (codec == null) { String message = "WaveletCodec is null"; Logging.logger().severe(message); throw new IllegalArgumentException(message); } int length = (4 * Integer.SIZE) / 8; for (int k = 0; k < codec.xform.length; k++) { length += (codec.xform[k].length * Byte.SIZE) / 8; } java.nio.ByteBuffer buffer = java.nio.ByteBuffer.allocate(length); buffer.putInt(codec.resolutionX); buffer.putInt(codec.resolutionY); buffer.putInt(codec.type); buffer.putInt(codec.xform.length); // Number of bands. for (int k = 0; k < codec.xform.length; k++) { buffer.put(codec.xform[k], 0, codec.xform[k].length); } buffer.flip(); return buffer; } private static boolean isTypeValid(int type) { return type == TYPE_BYTE_GRAY || type == TYPE_3BYTE_BGR || type == TYPE_4BYTE_ARGB; } private static int getBufferedImageType(WaveletCodec codec) { if (codec == null) { String message = "WaveletCodec is null"; Logging.logger().severe(message); throw new IllegalArgumentException(message); } int biType = -1; switch (codec.type) { case TYPE_BYTE_GRAY: biType = BufferedImage.TYPE_BYTE_GRAY; break; case TYPE_3BYTE_BGR: biType = BufferedImage.TYPE_3BYTE_BGR; break; case TYPE_4BYTE_ARGB: biType = BufferedImage.TYPE_4BYTE_ABGR; break; } return biType; } private static int getWaveletType(BufferedImage image) { if (image == null) { String message = "BufferedImage is null"; Logging.logger().severe(message); throw new IllegalArgumentException(message); } int type = -1; switch (image.getType()) { case BufferedImage.TYPE_BYTE_GRAY: type = TYPE_BYTE_GRAY; break; case BufferedImage.TYPE_3BYTE_BGR: type = TYPE_3BYTE_BGR; break; case BufferedImage.TYPE_4BYTE_ABGR: type = TYPE_4BYTE_ARGB; break; } return type; } public static WaveletCodec load(java.nio.ByteBuffer buffer) throws IOException { if (buffer == null) { String message = Logging.getMessage("nullValue.ByteBufferIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } int resolutionX = buffer.getInt(); int resolutionY = buffer.getInt(); int type = buffer.getInt(); if (!isTypeValid(type)) throw new IllegalArgumentException("WaveletCodec.loadFully(): invalid encoding type"); int numBands = buffer.getInt(); byte[][] xform = new byte[numBands][resolutionX * resolutionY]; for (int k = 0; k < numBands; k++) { buffer.get(xform[k], 0, xform[k].length); } WaveletCodec codec = new WaveletCodec(type, resolutionX, resolutionY); codec.xform = xform; return codec; } public static WaveletCodec loadPartial(java.nio.ByteBuffer buffer, int resolution) throws IOException { if (buffer == null) { String message = Logging.getMessage("nullValue.ByteBufferIsNull"); Logging.logger().severe(message); throw new IllegalArgumentException(message); } int resolutionX = buffer.getInt(); int resolutionY = buffer.getInt(); if (resolution > resolutionX || resolution > resolutionY) throw new IllegalArgumentException("WaveletCodec.loadPartially(): input resolution greater than encoded image"); int type = buffer.getInt(); if (!isTypeValid(type)) throw new IllegalArgumentException("WaveletCodec.loadPartially(): invalid encoding type"); int numBands = buffer.getInt(); byte[][] xform = new byte[numBands][resolution*resolution]; for (int k = 0; k < numBands; k++) { buffer.position(4*(Integer.SIZE/Byte.SIZE) + k * (resolutionX * resolutionY)); buffer.get(xform[k], 0, xform[k].length); } WaveletCodec codec = new WaveletCodec(type, resolutionX, resolutionY); codec.xform = xform; return codec; } /** * Creates a wavelet encoding from the given BufferedImage. The image must have dimensions that are * a power of 2. If the incoming image has at least 3 bands, the first three are assumed to be RGB channels. * If only one-band, it is assumed to be grayscale. The SampleModel component-type must be BYTE. * * @param image * @return * @throws IllegalArgumentException */ public static WaveletCodec encode(BufferedImage image) throws IllegalArgumentException { if (image == null) throw new IllegalArgumentException("WaveletCodec.encode: null image"); // Does image have the required resolution constraints? int xRes = image.getWidth(); int yRes = image.getHeight(); if (!WWMath.isPowerOfTwo(xRes) || !WWMath.isPowerOfTwo(yRes)) throw new IllegalArgumentException("Image dimensions are not a power of 2"); // Try to determine image type... SampleModel sampleModel = image.getSampleModel(); int numBands = sampleModel.getNumBands(); if ( !(numBands == 1 || numBands == 3 || numBands == 4) || sampleModel.getDataType() != DataBuffer.TYPE_BYTE) throw new IllegalArgumentException("Image is not of BYTE type, or not recognized as grayscale, RGB, or ARGB"); int type = getWaveletType(image); if (!isTypeValid(type)) throw new IllegalArgumentException("Image is not recognized as grayscale, RGB, or ARGB"); // Looks good to go; grab the image data. We'll need to make a copy, as we need some // temp working space and we don't want to corrupt the BufferedImage's data... int bandSize = xRes * yRes; //int next = 0; Raster rast = image.getRaster(); //float[] dataElems = new float[numBands]; float[][] imageData = new float[numBands][bandSize]; for (int k = 0; k < numBands; k++) { rast.getSamples(0, 0, xRes, yRes, k, imageData[k]); } //for (int j = 0; j < yRes; j++) { // for (int i = 0; i < xRes; i++) { // rast.getPixel(i, j, dataElems); // for (int k = 0; k < numBands; k++) { // imageData[k][next] = dataElems[k]; // } // ++next; // } //} // We need some temporary work space the size of the image... float[][] workspace = new float[numBands][bandSize]; // Perform the transformation... int level = 0; int xformXres = xRes; int xformYres = yRes; while (true) { ++level; if ( !(xformXres > 0 || xformYres > 0)) break; int halfXformXres = xformXres / 2; int halfXformYres = xformYres / 2; // transform along the rows... for (int j = 0; j < xformYres; j++) { int offset = j * yRes; // IMPORTANT THAT THIS REFLECT SOURCE IMAGE, NOT THE CURRENT LEVEL! for (int i = 0; i < halfXformXres; i++) { int indx1 = offset + i*2; int indx2 = offset + i*2 + 1; // horizontally... for (int k = 0; k < numBands; k++) { float average = (imageData[k][indx1] + imageData[k][indx2]) / 2f; float detail = imageData[k][indx1] - average; workspace[k][offset + i] = average; workspace[k][offset + i + halfXformXres] = detail; } } } // copy transformed data from this iteration back into our source arrays... for (int k=0; k < numBands; k++) System.arraycopy(workspace[k], 0, imageData[k], 0, workspace[k].length); // now transform along columns... for (int j = 0; j < xformXres; j++) { for (int i = 0; i < halfXformYres; i++) { int indx1 = j + (i*2)*yRes; int indx2 = j + (i*2+1)*yRes; // horizontally... for (int k = 0; k < numBands; k++) { float average = (imageData[k][indx1] + imageData[k][indx2]) / 2f; float detail = imageData[k][indx1] - average; workspace[k][j + i*yRes] = average; workspace[k][j + (i+halfXformYres)*yRes] = detail; } } } xformXres /= 2; xformYres /= 2; // copy transformed data from this iteration back into our source arrays... for (int k=0; k < numBands; k++) System.arraycopy(workspace[k], 0, imageData[k], 0, workspace[k].length); } // Our return WaveletCodec... WaveletCodec codec = new WaveletCodec(type, xRes, yRes); codec.xform = new byte[numBands][bandSize]; // // Rearrange in memory for optimal, hierarchical layout on disk, quantizing down to // byte values as we go. // // NOTE: the first byte of each channel is different; it represents the average color of the // overall image, and as such should be an unsigned quantity in the range 0..255. // All other values are signed coefficents, so the clamping boundaries are different. for (int k=0; k<numBands; k++) codec.xform[k][0] = (byte) Math.min(255, Math.max(0, Math.round(imageData[k][0]))); int scale = 1; // actually inverse of the magnification level... int next = 1; while (scale < xRes) { for (int subBlock = 0; subBlock < 3; subBlock++) { int colOffset = ((subBlock % 2) == 0) ? scale : 0; int rowOffset = (subBlock > 0) ? scale * xRes : 0; for (int j = 0; j < scale; j++) { for (int i = 0; i < scale; i++, next++) { int indx = rowOffset + colOffset + j*xRes + i; for (int k = 0; k < numBands; k++) { codec.xform[k][next] = (byte) Math.max(Byte.MIN_VALUE, Math.min(Byte.MAX_VALUE, Math.round(imageData[k][indx]))); } } } } scale *= 2; } // Done! return codec; } }