001 /* AffineTransformOp.java -- This class performs affine
002 transformation between two images or rasters in 2 dimensions.
003 Copyright (C) 2004, 2006 Free Software Foundation
004
005 This file is part of GNU Classpath.
006
007 GNU Classpath is free software; you can redistribute it and/or modify
008 it under the terms of the GNU General Public License as published by
009 the Free Software Foundation; either version 2, or (at your option)
010 any later version.
011
012 GNU Classpath is distributed in the hope that it will be useful, but
013 WITHOUT ANY WARRANTY; without even the implied warranty of
014 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 General Public License for more details.
016
017 You should have received a copy of the GNU General Public License
018 along with GNU Classpath; see the file COPYING. If not, write to the
019 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
020 02110-1301 USA.
021
022 Linking this library statically or dynamically with other modules is
023 making a combined work based on this library. Thus, the terms and
024 conditions of the GNU General Public License cover the whole
025 combination.
026
027 As a special exception, the copyright holders of this library give you
028 permission to link this library with independent modules to produce an
029 executable, regardless of the license terms of these independent
030 modules, and to copy and distribute the resulting executable under
031 terms of your choice, provided that you also meet, for each linked
032 independent module, the terms and conditions of the license of that
033 module. An independent module is a module which is not derived from
034 or based on this library. If you modify this library, you may extend
035 this exception to your version of the library, but you are not
036 obligated to do so. If you do not wish to do so, delete this
037 exception statement from your version. */
038
039 package java.awt.image;
040
041 import java.awt.Graphics2D;
042 import java.awt.Point;
043 import java.awt.Rectangle;
044 import java.awt.RenderingHints;
045 import java.awt.geom.AffineTransform;
046 import java.awt.geom.NoninvertibleTransformException;
047 import java.awt.geom.Point2D;
048 import java.awt.geom.Rectangle2D;
049 import java.util.Arrays;
050
051 /**
052 * AffineTransformOp performs matrix-based transformations (translations,
053 * scales, flips, rotations, and shears).
054 *
055 * If interpolation is required, nearest neighbour, bilinear, and bicubic
056 * methods are available.
057 *
058 * @author Olga Rodimina (rodimina@redhat.com)
059 * @author Francis Kung (fkung@redhat.com)
060 */
061 public class AffineTransformOp implements BufferedImageOp, RasterOp
062 {
063 public static final int TYPE_NEAREST_NEIGHBOR = 1;
064
065 public static final int TYPE_BILINEAR = 2;
066
067 /**
068 * @since 1.5.0
069 */
070 public static final int TYPE_BICUBIC = 3;
071
072 private AffineTransform transform;
073 private RenderingHints hints;
074
075 /**
076 * Construct AffineTransformOp with the given xform and interpolationType.
077 * Interpolation type can be TYPE_BILINEAR, TYPE_BICUBIC or
078 * TYPE_NEAREST_NEIGHBOR.
079 *
080 * @param xform AffineTransform that will applied to the source image
081 * @param interpolationType type of interpolation used
082 * @throws ImagingOpException if the transform matrix is noninvertible
083 */
084 public AffineTransformOp (AffineTransform xform, int interpolationType)
085 {
086 this.transform = xform;
087 if (xform.getDeterminant() == 0)
088 throw new ImagingOpException(null);
089
090 switch (interpolationType)
091 {
092 case TYPE_BILINEAR:
093 hints = new RenderingHints (RenderingHints.KEY_INTERPOLATION,
094 RenderingHints.VALUE_INTERPOLATION_BILINEAR);
095 break;
096 case TYPE_BICUBIC:
097 hints = new RenderingHints (RenderingHints.KEY_INTERPOLATION,
098 RenderingHints.VALUE_INTERPOLATION_BICUBIC);
099 break;
100 default:
101 hints = new RenderingHints (RenderingHints.KEY_INTERPOLATION,
102 RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
103 }
104 }
105
106 /**
107 * Construct AffineTransformOp with the given xform and rendering hints.
108 *
109 * @param xform AffineTransform that will applied to the source image
110 * @param hints rendering hints that will be used during transformation
111 * @throws ImagingOpException if the transform matrix is noninvertible
112 */
113 public AffineTransformOp (AffineTransform xform, RenderingHints hints)
114 {
115 this.transform = xform;
116 this.hints = hints;
117 if (xform.getDeterminant() == 0)
118 throw new ImagingOpException(null);
119 }
120
121 /**
122 * Creates a new BufferedImage with the size equal to that of the
123 * transformed image and the correct number of bands. The newly created
124 * image is created with the specified ColorModel.
125 * If a ColorModel is not specified, an appropriate ColorModel is used.
126 *
127 * @param src the source image.
128 * @param destCM color model for the destination image (can be null).
129 * @return a new compatible destination image.
130 */
131 public BufferedImage createCompatibleDestImage (BufferedImage src,
132 ColorModel destCM)
133 {
134 if (destCM != null)
135 return new BufferedImage(destCM,
136 createCompatibleDestRaster(src.getRaster()),
137 src.isAlphaPremultiplied(), null);
138
139 // This behaviour was determined by Mauve testcases, and is compatible
140 // with the reference implementation
141 if (src.getType() == BufferedImage.TYPE_INT_ARGB_PRE
142 || src.getType() == BufferedImage.TYPE_4BYTE_ABGR
143 || src.getType() == BufferedImage.TYPE_4BYTE_ABGR_PRE)
144 return new BufferedImage(src.getWidth(), src.getHeight(), src.getType());
145
146 else
147 return new BufferedImage(src.getWidth(), src.getHeight(),
148 BufferedImage.TYPE_INT_ARGB);
149 }
150
151 /**
152 * Creates a new WritableRaster with the size equal to the transformed
153 * source raster and correct number of bands .
154 *
155 * @param src the source raster.
156 * @throws RasterFormatException if resulting width or height of raster is 0.
157 * @return a new compatible raster.
158 */
159 public WritableRaster createCompatibleDestRaster (Raster src)
160 {
161 Rectangle2D rect = getBounds2D(src);
162
163 if (rect.getWidth() == 0 || rect.getHeight() == 0)
164 throw new RasterFormatException("width or height is 0");
165
166 return src.createCompatibleWritableRaster((int) rect.getWidth(),
167 (int) rect.getHeight());
168 }
169
170 /**
171 * Transforms source image using transform specified at the constructor.
172 * The resulting transformed image is stored in the destination image if one
173 * is provided; otherwise a new BufferedImage is created and returned.
174 *
175 * @param src source image
176 * @param dst destination image
177 * @throws IllegalArgumentException if the source and destination image are
178 * the same
179 * @return transformed source image.
180 */
181 public final BufferedImage filter (BufferedImage src, BufferedImage dst)
182 {
183 if (dst == src)
184 throw new IllegalArgumentException("src image cannot be the same as "
185 + "the dst image");
186
187 // If the destination image is null, then use a compatible BufferedImage
188 if (dst == null)
189 dst = createCompatibleDestImage(src, null);
190
191 Graphics2D gr = dst.createGraphics();
192 gr.setRenderingHints(hints);
193 gr.drawImage(src, transform, null);
194 return dst;
195 }
196
197 /**
198 * Transforms source raster using transform specified at the constructor.
199 * The resulting raster is stored in the destination raster if it is not
200 * null, otherwise a new raster is created and returned.
201 *
202 * @param src source raster
203 * @param dst destination raster
204 * @throws IllegalArgumentException if the source and destination are not
205 * compatible
206 * @return transformed raster.
207 */
208 public final WritableRaster filter(Raster src, WritableRaster dst)
209 {
210 // Initial checks
211 if (dst == src)
212 throw new IllegalArgumentException("src image cannot be the same as"
213 + " the dst image");
214
215 if (dst == null)
216 dst = createCompatibleDestRaster(src);
217
218 if (src.getNumBands() != dst.getNumBands())
219 throw new IllegalArgumentException("src and dst must have same number"
220 + " of bands");
221
222 // Optimization for rasters that can be represented in the RGB colormodel:
223 // wrap the rasters in images, and let Cairo do the transformation
224 if (ColorModel.getRGBdefault().isCompatibleSampleModel(src.getSampleModel())
225 && ColorModel.getRGBdefault().isCompatibleSampleModel(dst.getSampleModel()))
226 {
227 WritableRaster src2 = Raster.createWritableRaster(src.getSampleModel(),
228 src.getDataBuffer(),
229 new Point(src.getMinX(),
230 src.getMinY()));
231 BufferedImage iSrc = new BufferedImage(ColorModel.getRGBdefault(),
232 src2, false, null);
233 BufferedImage iDst = new BufferedImage(ColorModel.getRGBdefault(), dst,
234 false, null);
235
236 return filter(iSrc, iDst).getRaster();
237 }
238
239 // Otherwise, we need to do the transformation in java code...
240 // Create arrays to hold all the points
241 double[] dstPts = new double[dst.getHeight() * dst.getWidth() * 2];
242 double[] srcPts = new double[dst.getHeight() * dst.getWidth() * 2];
243
244 // Populate array with all points in the *destination* raster
245 int i = 0;
246 for (int x = 0; x < dst.getWidth(); x++)
247 {
248 for (int y = 0; y < dst.getHeight(); y++)
249 {
250 dstPts[i++] = x;
251 dstPts[i++] = y;
252 }
253 }
254 Rectangle srcbounds = src.getBounds();
255
256 // Use an inverse transform to map each point in the destination to
257 // a point in the source. Note that, while all points in the destination
258 // matrix are integers, this is not necessarily true for points in the
259 // source (hence why interpolation is required)
260 try
261 {
262 AffineTransform inverseTx = transform.createInverse();
263 inverseTx.transform(dstPts, 0, srcPts, 0, dstPts.length / 2);
264 }
265 catch (NoninvertibleTransformException e)
266 {
267 // Shouldn't happen since the constructor traps this
268 throw new ImagingOpException(e.getMessage());
269 }
270
271 // Different interpolation methods...
272 if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR))
273 filterNearest(src, dst, dstPts, srcPts);
274
275 else if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BILINEAR))
276 filterBilinear(src, dst, dstPts, srcPts);
277
278 else // bicubic
279 filterBicubic(src, dst, dstPts, srcPts);
280
281 return dst;
282 }
283
284 /**
285 * Transforms source image using transform specified at the constructor and
286 * returns bounds of the transformed image.
287 *
288 * @param src image to be transformed
289 * @return bounds of the transformed image.
290 */
291 public final Rectangle2D getBounds2D (BufferedImage src)
292 {
293 return getBounds2D (src.getRaster());
294 }
295
296 /**
297 * Returns bounds of the transformed raster.
298 *
299 * @param src raster to be transformed
300 * @return bounds of the transformed raster.
301 */
302 public final Rectangle2D getBounds2D (Raster src)
303 {
304 return transform.createTransformedShape(src.getBounds()).getBounds2D();
305 }
306
307 /**
308 * Returns interpolation type used during transformations.
309 *
310 * @return interpolation type
311 */
312 public final int getInterpolationType ()
313 {
314 if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BILINEAR))
315 return TYPE_BILINEAR;
316
317 else if (hints.containsValue(RenderingHints.VALUE_INTERPOLATION_BICUBIC))
318 return TYPE_BICUBIC;
319
320 else
321 return TYPE_NEAREST_NEIGHBOR;
322 }
323
324 /**
325 * Returns location of the transformed source point. The resulting point
326 * is stored in the dstPt if one is specified.
327 *
328 * @param srcPt point to be transformed
329 * @param dstPt destination point
330 * @return the location of the transformed source point.
331 */
332 public final Point2D getPoint2D (Point2D srcPt, Point2D dstPt)
333 {
334 return transform.transform (srcPt, dstPt);
335 }
336
337 /**
338 * Returns rendering hints that are used during transformation.
339 *
340 * @return the rendering hints used in this Op.
341 */
342 public final RenderingHints getRenderingHints ()
343 {
344 return hints;
345 }
346
347 /**
348 * Returns transform used in transformation between source and destination
349 * image.
350 *
351 * @return the transform used in this Op.
352 */
353 public final AffineTransform getTransform ()
354 {
355 return transform;
356 }
357
358 /**
359 * Perform nearest-neighbour filtering
360 *
361 * @param src the source raster
362 * @param dst the destination raster
363 * @param dpts array of points on the destination raster
364 * @param pts array of corresponding points on the source raster
365 */
366 private void filterNearest(Raster src, WritableRaster dst, double[] dpts,
367 double[] pts)
368 {
369 Rectangle srcbounds = src.getBounds();
370
371 // For all points on the destination raster, copy the value from the
372 // corrosponding (rounded) source point
373 for (int i = 0; i < dpts.length; i += 2)
374 {
375 int srcX = (int) Math.round(pts[i]) + src.getMinX();
376 int srcY = (int) Math.round(pts[i + 1]) + src.getMinY();
377
378 if (srcbounds.contains(srcX, srcY))
379 dst.setDataElements((int) dpts[i] + dst.getMinX(),
380 (int) dpts[i + 1] + dst.getMinY(),
381 src.getDataElements(srcX, srcY, null));
382 }
383 }
384
385 /**
386 * Perform bilinear filtering
387 *
388 * @param src the source raster
389 * @param dst the destination raster
390 * @param dpts array of points on the destination raster
391 * @param pts array of corresponding points on the source raster
392 */
393 private void filterBilinear(Raster src, WritableRaster dst, double[] dpts,
394 double[] pts)
395 {
396 Rectangle srcbounds = src.getBounds();
397
398 Object xyarr = null;
399 Object xp1arr = null;
400 Object yp1arr = null;
401 Object xyp1arr = null;
402
403 double xy;
404 double xp1;
405 double yp1;
406 double xyp1;
407
408 double[] result = new double[src.getNumBands()];
409
410 // For all points in the destination raster, use bilinear interpolation
411 // to find the value from the corrosponding source points
412 for (int i = 0; i < dpts.length; i += 2)
413 {
414 int srcX = (int) Math.round(pts[i]) + src.getMinX();
415 int srcY = (int) Math.round(pts[i + 1]) + src.getMinY();
416
417 if (srcbounds.contains(srcX, srcY))
418 {
419 // Corner case at the bottom or right edge; use nearest neighbour
420 if (pts[i] >= src.getWidth() - 1
421 || pts[i + 1] >= src.getHeight() - 1)
422 dst.setDataElements((int) dpts[i] + dst.getMinX(),
423 (int) dpts[i + 1] + dst.getMinY(),
424 src.getDataElements(srcX, srcY, null));
425
426 // Standard case, apply the bilinear formula
427 else
428 {
429 int x = (int) Math.floor(pts[i] + src.getMinX());
430 int y = (int) Math.floor(pts[i + 1] + src.getMinY());
431 double xdiff = pts[i] + src.getMinX() - x;
432 double ydiff = pts[i + 1] + src.getMinY() - y;
433
434 // Get surrounding pixels used in interpolation... optimized
435 // to use the smallest datatype possible.
436 if (src.getTransferType() == DataBuffer.TYPE_DOUBLE
437 || src.getTransferType() == DataBuffer.TYPE_FLOAT)
438 {
439 xyarr = src.getPixel(x, y, (double[])xyarr);
440 xp1arr = src.getPixel(x+1, y, (double[])xp1arr);
441 yp1arr = src.getPixel(x, y+1, (double[])yp1arr);
442 xyp1arr = src.getPixel(x+1, y+1, (double[])xyp1arr);
443 }
444 else
445 {
446 xyarr = src.getPixel(x, y, (int[])xyarr);
447 xp1arr = src.getPixel(x+1, y, (int[])xp1arr);
448 yp1arr = src.getPixel(x, y+1, (int[])yp1arr);
449 xyp1arr = src.getPixel(x+1, y+1, (int[])xyp1arr);
450 }
451 // using
452 // array[] pixels = src.getPixels(x, y, 2, 2, pixels);
453 // instead of doing four individual src.getPixel() calls
454 // should be faster, but benchmarking shows that it's not...
455
456 // Run interpolation for each band
457 for (int j = 0; j < src.getNumBands(); j++)
458 {
459 // Pull individual sample values out of array
460 if (src.getTransferType() == DataBuffer.TYPE_DOUBLE
461 || src.getTransferType() == DataBuffer.TYPE_FLOAT)
462 {
463 xy = ((double[])xyarr)[j];
464 xp1 = ((double[])xp1arr)[j];
465 yp1 = ((double[])yp1arr)[j];
466 xyp1 = ((double[])xyp1arr)[j];
467 }
468 else
469 {
470 xy = ((int[])xyarr)[j];
471 xp1 = ((int[])xp1arr)[j];
472 yp1 = ((int[])yp1arr)[j];
473 xyp1 = ((int[])xyp1arr)[j];
474 }
475
476 // If all four samples are identical, there's no need to
477 // calculate anything
478 if (xy == xp1 && xy == yp1 && xy == xyp1)
479 result[j] = xy;
480
481 // Run bilinear interpolation formula
482 else
483 result[j] = (xy * (1-xdiff) + xp1 * xdiff)
484 * (1-ydiff)
485 + (yp1 * (1-xdiff) + xyp1 * xdiff)
486 * ydiff;
487 }
488
489 dst.setPixel((int)dpts[i] + dst.getMinX(),
490 (int)dpts[i+1] + dst.getMinY(),
491 result);
492 }
493 }
494 }
495 }
496
497 /**
498 * Perform bicubic filtering
499 * based on http://local.wasp.uwa.edu.au/~pbourke/colour/bicubic/
500 *
501 * @param src the source raster
502 * @param dst the destination raster
503 * @param dpts array of points on the destination raster
504 * @param pts array of corresponding points on the source raster
505 */
506 private void filterBicubic(Raster src, WritableRaster dst, double[] dpts,
507 double[] pts)
508 {
509 Rectangle srcbounds = src.getBounds();
510 double[] result = new double[src.getNumBands()];
511 Object pixels = null;
512
513 // For all points on the destination raster, perform bicubic interpolation
514 // from corrosponding source points
515 for (int i = 0; i < dpts.length; i += 2)
516 {
517 if (srcbounds.contains((int) Math.round(pts[i]) + src.getMinX(),
518 (int) Math.round(pts[i + 1]) + src.getMinY()))
519 {
520 int x = (int) Math.floor(pts[i] + src.getMinX());
521 int y = (int) Math.floor(pts[i + 1] + src.getMinY());
522 double dx = pts[i] + src.getMinX() - x;
523 double dy = pts[i + 1] + src.getMinY() - y;
524 Arrays.fill(result, 0);
525
526 for (int m = - 1; m < 3; m++)
527 for (int n = - 1; n < 3; n++)
528 {
529 // R(x) = ( P(x+2)^3 - 4 P(x+1)^3 + 6 P(x)^3 - 4 P(x-1)^3 ) / 6
530 double r1 = 0;
531 double r2 = 0;
532
533 // Calculate R(m - dx)
534 double rx = m - dx + 2;
535 r1 += rx * rx * rx;
536
537 rx = m - dx + 1;
538 if (rx > 0)
539 r1 -= 4 * rx * rx * rx;
540
541 rx = m - dx;
542 if (rx > 0)
543 r1 += 6 * rx * rx * rx;
544
545 rx = m - dx - 1;
546 if (rx > 0)
547 r1 -= 4 * rx * rx * rx;
548
549 r1 /= 6;
550
551 // Calculate R(dy - n);
552 rx = dy - n + 2;
553 if (rx > 0)
554 r2 += rx * rx * rx;
555
556 rx = dy - n + 1;
557 if (rx > 0)
558 r2 -= 4 * rx * rx * rx;
559
560 rx = dy - n;
561 if (rx > 0)
562 r2 += 6 * rx * rx * rx;
563
564 rx = dy - n - 1;
565 if (rx > 0)
566 r2 -= 4 * rx * rx * rx;
567
568 r2 /= 6;
569
570 // Calculate F(i+m, j+n) R(m - dx) R(dy - n)
571 // Check corner cases
572 int srcX = x + m;
573 if (srcX >= src.getMinX() + src.getWidth())
574 srcX = src.getMinX() + src.getWidth() - 1;
575 else if (srcX < src.getMinX())
576 srcX = src.getMinX();
577
578 int srcY = y + n;
579 if (srcY >= src.getMinY() + src.getHeight())
580 srcY = src.getMinY() + src.getHeight() - 1;
581 else if (srcY < src.getMinY())
582 srcY = src.getMinY();
583
584 // Calculate once for each band, using the smallest
585 // datatype possible
586 if (src.getTransferType() == DataBuffer.TYPE_DOUBLE
587 || src.getTransferType() == DataBuffer.TYPE_FLOAT)
588 {
589 pixels = src.getPixel(srcX, srcY, (double[])pixels);
590 for (int j = 0; j < result.length; j++)
591 result[j] += ((double[])pixels)[j] * r1 * r2;
592 }
593 else
594 {
595 pixels = src.getPixel(srcX, srcY, (int[])pixels);
596 for (int j = 0; j < result.length; j++)
597 result[j] += ((int[])pixels)[j] * r1 * r2;
598 }
599 }
600
601 // Put it all together
602 dst.setPixel((int)dpts[i] + dst.getMinX(),
603 (int)dpts[i+1] + dst.getMinY(),
604 result);
605 }
606 }
607 }
608 }