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 = (Graphics2D) 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 }