Source for java.awt.image.ConvolveOp

   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: }