Frames | No Frames |
1: /* ConvolveOp.java -- 2: Copyright (C) 2004, 2005, 2006, Free Software Foundation -- ConvolveOp 3: 4: This file is part of GNU Classpath. 5: 6: GNU Classpath is free software; you can redistribute it and/or modify 7: it under the terms of the GNU General Public License as published by 8: the Free Software Foundation; either version 2, or (at your option) 9: any later version. 10: 11: GNU Classpath is distributed in the hope that it will be useful, but 12: WITHOUT ANY WARRANTY; without even the implied warranty of 13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14: General Public License for more details. 15: 16: You should have received a copy of the GNU General Public License 17: along with GNU Classpath; see the file COPYING. If not, write to the 18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 19: 02110-1301 USA. 20: 21: Linking this library statically or dynamically with other modules is 22: making a combined work based on this library. Thus, the terms and 23: conditions of the GNU General Public License cover the whole 24: combination. 25: 26: As a special exception, the copyright holders of this library give you 27: permission to link this library with independent modules to produce an 28: executable, regardless of the license terms of these independent 29: modules, and to copy and distribute the resulting executable under 30: terms of your choice, provided that you also meet, for each linked 31: independent module, the terms and conditions of the license of that 32: module. An independent module is a module which is not derived from 33: or based on this library. If you modify this library, you may extend 34: this exception to your version of the library, but you are not 35: obligated to do so. If you do not wish to do so, delete this 36: exception statement from your version. */ 37: 38: 39: package java.awt.image; 40: 41: import java.awt.RenderingHints; 42: import java.awt.geom.Point2D; 43: import java.awt.geom.Rectangle2D; 44: 45: /** 46: * Convolution filter. 47: * 48: * ConvolveOp convolves the source image with a Kernel to generate a 49: * destination image. This involves multiplying each pixel and its neighbors 50: * with elements in the kernel to compute a new pixel. 51: * 52: * Each band in a Raster is convolved and copied to the destination Raster. 53: * For BufferedImages, convolution is applied to all components. Color 54: * conversion will be applied if needed. 55: * 56: * Note that this filter ignores whether the source or destination is alpha 57: * premultiplied. The reference spec states that data will be premultiplied 58: * prior to convolving and divided back out afterwards (if needed), but testing 59: * has shown that this is not the case with their implementation. 60: * 61: * @author jlquinn@optonline.net 62: */ 63: public class ConvolveOp implements BufferedImageOp, RasterOp 64: { 65: /** Edge pixels are set to 0. */ 66: public static final int EDGE_ZERO_FILL = 0; 67: 68: /** Edge pixels are copied from the source. */ 69: public static final int EDGE_NO_OP = 1; 70: 71: private Kernel kernel; 72: private int edge; 73: private RenderingHints hints; 74: 75: /** 76: * Construct a ConvolveOp. 77: * 78: * The edge condition specifies that pixels outside the area that can be 79: * filtered are either set to 0 or copied from the source image. 80: * 81: * @param kernel The kernel to convolve with. 82: * @param edgeCondition Either EDGE_ZERO_FILL or EDGE_NO_OP. 83: * @param hints Rendering hints for color conversion, or null. 84: */ 85: public ConvolveOp(Kernel kernel, 86: int edgeCondition, 87: RenderingHints hints) 88: { 89: this.kernel = kernel; 90: edge = edgeCondition; 91: this.hints = hints; 92: } 93: 94: /** 95: * Construct a ConvolveOp. 96: * 97: * The edge condition defaults to EDGE_ZERO_FILL. 98: * 99: * @param kernel The kernel to convolve with. 100: */ 101: public ConvolveOp(Kernel kernel) 102: { 103: this.kernel = kernel; 104: edge = EDGE_ZERO_FILL; 105: hints = null; 106: } 107: 108: /** 109: * Converts the source image using the kernel specified in the 110: * constructor. The resulting image is stored in the destination image if one 111: * is provided; otherwise a new BufferedImage is created and returned. 112: * 113: * The source and destination BufferedImage (if one is supplied) must have 114: * the same dimensions. 115: * 116: * @param src The source image. 117: * @param dst The destination image. 118: * @throws IllegalArgumentException if the rasters and/or color spaces are 119: * incompatible. 120: * @return The convolved image. 121: */ 122: public final BufferedImage filter(BufferedImage src, BufferedImage dst) 123: { 124: if (src == dst) 125: throw new IllegalArgumentException("Source and destination images " + 126: "cannot be the same."); 127: 128: if (dst == null) 129: dst = createCompatibleDestImage(src, src.getColorModel()); 130: 131: // Make sure source image is premultiplied 132: BufferedImage src1 = src; 133: // The spec says we should do this, but mauve testing shows that Sun's 134: // implementation does not check this. 135: /* 136: if (!src.isAlphaPremultiplied()) 137: { 138: src1 = createCompatibleDestImage(src, src.getColorModel()); 139: src.copyData(src1.getRaster()); 140: src1.coerceData(true); 141: } 142: */ 143: 144: BufferedImage dst1 = dst; 145: if (src1.getColorModel().getColorSpace().getType() != dst.getColorModel().getColorSpace().getType()) 146: dst1 = createCompatibleDestImage(src, src.getColorModel()); 147: 148: filter(src1.getRaster(), dst1.getRaster()); 149: 150: // Since we don't coerceData above, we don't need to divide it back out. 151: // This is wrong (one mauve test specifically tests converting a non- 152: // premultiplied image to a premultiplied image, and it shows that Sun 153: // simply ignores the premultipled flag, contrary to the spec), but we 154: // mimic it for compatibility. 155: /* 156: if (! dst.isAlphaPremultiplied()) 157: dst1.coerceData(false); 158: */ 159: 160: // Convert between color models if needed 161: if (dst1 != dst) 162: new ColorConvertOp(hints).filter(dst1, dst); 163: 164: return dst; 165: } 166: 167: /** 168: * Creates an empty BufferedImage with the size equal to the source and the 169: * correct number of bands. The new image is created with the specified 170: * ColorModel, or if no ColorModel is supplied, an appropriate one is chosen. 171: * 172: * @param src The source image. 173: * @param dstCM A color model for the destination image (may be null). 174: * @return The new compatible destination image. 175: */ 176: public BufferedImage createCompatibleDestImage(BufferedImage src, 177: ColorModel dstCM) 178: { 179: if (dstCM != null) 180: return new BufferedImage(dstCM, 181: src.getRaster().createCompatibleWritableRaster(), 182: src.isAlphaPremultiplied(), null); 183: 184: return new BufferedImage(src.getWidth(), src.getHeight(), src.getType()); 185: } 186: 187: /* (non-Javadoc) 188: * @see java.awt.image.RasterOp#getRenderingHints() 189: */ 190: public final RenderingHints getRenderingHints() 191: { 192: return hints; 193: } 194: 195: /** 196: * Get the edge condition for this Op. 197: * 198: * @return The edge condition. 199: */ 200: public int getEdgeCondition() 201: { 202: return edge; 203: } 204: 205: /** 206: * Returns (a clone of) the convolution kernel. 207: * 208: * @return The convolution kernel. 209: */ 210: public final Kernel getKernel() 211: { 212: return (Kernel) kernel.clone(); 213: } 214: 215: /** 216: * Converts the source raster using the kernel specified in the constructor. 217: * The resulting raster is stored in the destination raster if one is 218: * provided; otherwise a new WritableRaster is created and returned. 219: * 220: * If the convolved value for a sample is outside the range of [0-255], it 221: * will be clipped. 222: * 223: * The source and destination raster (if one is supplied) cannot be the same, 224: * and must also have the same dimensions. 225: * 226: * @param src The source raster. 227: * @param dest The destination raster. 228: * @throws IllegalArgumentException if the rasters identical. 229: * @throws ImagingOpException if the convolution is not possible. 230: * @return The transformed raster. 231: */ 232: public final WritableRaster filter(Raster src, WritableRaster dest) 233: { 234: if (src == dest) 235: throw new IllegalArgumentException("src == dest is not allowed."); 236: if (kernel.getWidth() > src.getWidth() 237: || kernel.getHeight() > src.getHeight()) 238: throw new ImagingOpException("The kernel is too large."); 239: if (dest == null) 240: dest = createCompatibleDestRaster(src); 241: else if (src.getNumBands() != dest.getNumBands()) 242: throw new ImagingOpException("src and dest have different band counts."); 243: 244: // calculate the borders that the op can't reach... 245: int kWidth = kernel.getWidth(); 246: int kHeight = kernel.getHeight(); 247: int left = kernel.getXOrigin(); 248: int right = Math.max(kWidth - left - 1, 0); 249: int top = kernel.getYOrigin(); 250: int bottom = Math.max(kHeight - top - 1, 0); 251: 252: // Calculate max sample values for clipping 253: int[] maxValue = src.getSampleModel().getSampleSize(); 254: for (int i = 0; i < maxValue.length; i++) 255: maxValue[i] = (int)Math.pow(2, maxValue[i]) - 1; 256: 257: // process the region that is reachable... 258: int regionW = src.width - left - right; 259: int regionH = src.height - top - bottom; 260: float[] kvals = kernel.getKernelData(null); 261: float[] tmp = new float[kWidth * kHeight]; 262: 263: for (int x = 0; x < regionW; x++) 264: { 265: for (int y = 0; y < regionH; y++) 266: { 267: // FIXME: This needs a much more efficient implementation 268: for (int b = 0; b < src.getNumBands(); b++) 269: { 270: float v = 0; 271: src.getSamples(x, y, kWidth, kHeight, b, tmp); 272: for (int i = 0; i < tmp.length; i++) 273: v += tmp[tmp.length - i - 1] * kvals[i]; 274: // FIXME: in the above line, I've had to reverse the order of 275: // the samples array to make the tests pass. I haven't worked 276: // out why this is necessary. 277: 278: // This clipping is is undocumented, but determined by testing. 279: if (v > maxValue[b]) 280: v = maxValue[b]; 281: else if (v < 0) 282: v = 0; 283: 284: dest.setSample(x + kernel.getXOrigin(), y + kernel.getYOrigin(), 285: b, v); 286: } 287: } 288: } 289: 290: // fill in the top border 291: fillEdge(src, dest, 0, 0, src.width, top, edge); 292: 293: // fill in the bottom border 294: fillEdge(src, dest, 0, src.height - bottom, src.width, bottom, edge); 295: 296: // fill in the left border 297: fillEdge(src, dest, 0, top, left, regionH, edge); 298: 299: // fill in the right border 300: fillEdge(src, dest, src.width - right, top, right, regionH, edge); 301: 302: return dest; 303: } 304: 305: /** 306: * Fills a range of pixels (typically at the edge of a raster) with either 307: * zero values (if <code>edgeOp</code> is <code>EDGE_ZERO_FILL</code>) or the 308: * corresponding pixel values from the source raster (if <code>edgeOp</code> 309: * is <code>EDGE_NO_OP</code>). This utility method is called by the 310: * {@link #fillEdge(Raster, WritableRaster, int, int, int, int, int)} method. 311: * 312: * @param src the source raster. 313: * @param dest the destination raster. 314: * @param x the x-coordinate of the top left pixel in the range. 315: * @param y the y-coordinate of the top left pixel in the range. 316: * @param w the width of the pixel range. 317: * @param h the height of the pixel range. 318: * @param edgeOp indicates how to determine the values for the range 319: * (either {@link #EDGE_ZERO_FILL} or {@link #EDGE_NO_OP}). 320: */ 321: private void fillEdge(Raster src, WritableRaster dest, int x, int y, int w, 322: int h, int edgeOp) 323: { 324: if (w <= 0) 325: return; 326: if (h <= 0) 327: return; 328: if (edgeOp == EDGE_ZERO_FILL) // fill region with zeroes 329: { 330: float[] zeros = new float[src.getNumBands() * w * h]; 331: dest.setPixels(x, y, w, h, zeros); 332: } 333: else // copy pixels from source 334: { 335: float[] pixels = new float[src.getNumBands() * w * h]; 336: src.getPixels(x, y, w, h, pixels); 337: dest.setPixels(x, y, w, h, pixels); 338: } 339: } 340: 341: /* (non-Javadoc) 342: * @see java.awt.image.RasterOp#createCompatibleDestRaster(java.awt.image.Raster) 343: */ 344: public WritableRaster createCompatibleDestRaster(Raster src) 345: { 346: return src.createCompatibleWritableRaster(); 347: } 348: 349: /* (non-Javadoc) 350: * @see java.awt.image.BufferedImageOp#getBounds2D(java.awt.image.BufferedImage) 351: */ 352: public final Rectangle2D getBounds2D(BufferedImage src) 353: { 354: return src.getRaster().getBounds(); 355: } 356: 357: /* (non-Javadoc) 358: * @see java.awt.image.RasterOp#getBounds2D(java.awt.image.Raster) 359: */ 360: public final Rectangle2D getBounds2D(Raster src) 361: { 362: return src.getBounds(); 363: } 364: 365: /** 366: * Returns the corresponding destination point for a source point. Because 367: * this is not a geometric operation, the destination and source points will 368: * be identical. 369: * 370: * @param src The source point. 371: * @param dst The transformed destination point. 372: * @return The transformed destination point. 373: */ 374: public final Point2D getPoint2D(Point2D src, Point2D dst) 375: { 376: if (dst == null) return (Point2D)src.clone(); 377: dst.setLocation(src); 378: return dst; 379: } 380: }