package com.hphoto.image; import java.awt.*; import java.awt.image.*; /** * An abstract superclass for filters which distort images in some way. The subclass only needs to override * two methods to provide the mapping between source and destination pixels. */ public abstract class TransformFilter extends AbstractBufferedImageOp { /** * Treat pixels off the edge as zero. */ public final static int ZERO = 0; /** * Clamp pixels to the image edges. */ public final static int CLAMP = 1; /** * Wrap pixels off the edge onto the oppsoite edge. */ public final static int WRAP = 2; /** * Use nearest-neighbout interpolation. */ public final static int NEAREST_NEIGHBOUR = 0; /** * Use bilinear interpolation. */ public final static int BILINEAR = 1; /** * The action to take for pixels off the image edge. */ protected int edgeAction = ZERO; /** * The type of interpolation to use. */ protected int interpolation = BILINEAR; /** * The output image rectangle. */ protected Rectangle transformedSpace; /** * The input image rectangle. */ protected Rectangle originalSpace; /** * Set the action to perform for pixels off the edge of the image. * @param edgeAction one of ZERO, CLAMP or WRAP * @see #getEdgeAction */ public void setEdgeAction(int edgeAction) { this.edgeAction = edgeAction; } /** * Get the action to perform for pixels off the edge of the image. * @return one of ZERO, CLAMP or WRAP * @see #setEdgeAction */ public int getEdgeAction() { return edgeAction; } /** * Set the type of interpolation to perform. * @param interpolation one of NEAREST_NEIGHBOUR or BILINEAR * @see #getInterpolation */ public void setInterpolation(int interpolation) { this.interpolation = interpolation; } /** * Get the type of interpolation to perform. * @return one of NEAREST_NEIGHBOUR or BILINEAR * @see #setInterpolation */ public int getInterpolation() { return interpolation; } /** * Inverse transform a point. This method needs to be overriden by all subclasses. * @param x the X position of the pixel in the output image * @param y the Y position of the pixel in the output image * @param out the position of the pixel in the input image */ protected abstract void transformInverse(int x, int y, float[] out); /** * Forward transform a rectangle. Used to determine the size of the output image. * @param rect the rectangle to transform */ protected void transformSpace(Rectangle rect) { } public BufferedImage filter( BufferedImage src, BufferedImage dst ) { int width = src.getWidth(); int height = src.getHeight(); int type = src.getType(); WritableRaster srcRaster = src.getRaster(); originalSpace = new Rectangle(0, 0, width, height); transformedSpace = new Rectangle(0, 0, width, height); transformSpace(transformedSpace); if ( dst == null ) { ColorModel dstCM = src.getColorModel(); dst = new BufferedImage(dstCM, dstCM.createCompatibleWritableRaster(transformedSpace.width, transformedSpace.height), dstCM.isAlphaPremultiplied(), null); } WritableRaster dstRaster = dst.getRaster(); int[] inPixels = getRGB( src, 0, 0, width, height, null ); if ( interpolation == NEAREST_NEIGHBOUR ) return filterPixelsNN( dst, width, height, inPixels, transformedSpace ); int srcWidth = width; int srcHeight = height; int srcWidth1 = width-1; int srcHeight1 = height-1; int outWidth = transformedSpace.width; int outHeight = transformedSpace.height; int outX, outY; int index = 0; int[] outPixels = new int[outWidth]; outX = transformedSpace.x; outY = transformedSpace.y; float[] out = new float[2]; for (int y = 0; y < outHeight; y++) { for (int x = 0; x < outWidth; x++) { transformInverse(outX+x, outY+y, out); int srcX = (int)Math.floor( out[0] ); int srcY = (int)Math.floor( out[1] ); float xWeight = out[0]-srcX; float yWeight = out[1]-srcY; int nw, ne, sw, se; if ( srcX >= 0 && srcX < srcWidth1 && srcY >= 0 && srcY < srcHeight1) { // Easy case, all corners are in the image int i = srcWidth*srcY + srcX; nw = inPixels[i]; ne = inPixels[i+1]; sw = inPixels[i+srcWidth]; se = inPixels[i+srcWidth+1]; } else { // Some of the corners are off the image nw = getPixel( inPixels, srcX, srcY, srcWidth, srcHeight ); ne = getPixel( inPixels, srcX+1, srcY, srcWidth, srcHeight ); sw = getPixel( inPixels, srcX, srcY+1, srcWidth, srcHeight ); se = getPixel( inPixels, srcX+1, srcY+1, srcWidth, srcHeight ); } outPixels[x] = ImageMath.bilinearInterpolate(xWeight, yWeight, nw, ne, sw, se); } setRGB( dst, 0, y, transformedSpace.width, 1, outPixels ); } return dst; } final private int getPixel( int[] pixels, int x, int y, int width, int height ) { if (x < 0 || x >= width || y < 0 || y >= height) { switch (edgeAction) { case ZERO: default: return 0; case WRAP: return pixels[(ImageMath.mod(y, height) * width) + ImageMath.mod(x, width)]; case CLAMP: return pixels[(ImageMath.clamp(y, 0, height-1) * width) + ImageMath.clamp(x, 0, width-1)]; } } return pixels[ y*width+x ]; } protected BufferedImage filterPixelsNN( BufferedImage dst, int width, int height, int[] inPixels, Rectangle transformedSpace ) { int srcWidth = width; int srcHeight = height; int outWidth = transformedSpace.width; int outHeight = transformedSpace.height; int outX, outY, srcX, srcY; int[] outPixels = new int[outWidth]; outX = transformedSpace.x; outY = transformedSpace.y; int[] rgb = new int[4]; float[] out = new float[2]; for (int y = 0; y < outHeight; y++) { for (int x = 0; x < outWidth; x++) { transformInverse(outX+x, outY+y, out); srcX = (int)out[0]; srcY = (int)out[1]; // int casting rounds towards zero, so we check out[0] < 0, not srcX < 0 if (out[0] < 0 || srcX >= srcWidth || out[1] < 0 || srcY >= srcHeight) { int p; switch (edgeAction) { case ZERO: default: p = 0; break; case WRAP: p = inPixels[(ImageMath.mod(srcY, srcHeight) * srcWidth) + ImageMath.mod(srcX, srcWidth)]; break; case CLAMP: p = inPixels[(ImageMath.clamp(srcY, 0, srcHeight-1) * srcWidth) + ImageMath.clamp(srcX, 0, srcWidth-1)]; break; } outPixels[x] = p; } else { int i = srcWidth*srcY + srcX; rgb[0] = inPixels[i]; outPixels[x] = inPixels[i]; } } setRGB( dst, 0, y, transformedSpace.width, 1, outPixels ); } return dst; } }