Tuesday, July 29, 2008

Rotate an image in Java


In Python, many complex image functions are made simple using the Python Imaging Library (PIL). For example, I can load an image from file, rotate it any number of degrees, and display the image in just four lines of Python code.


from PIL import Image
pic = Image.open("wire.png")
pic.rotate(45)
pic.show()

Performing the same task in Java, however, requires the involvement of several classes and a slightly deeper understanding of graphics processing. I feel that Sun's decision to break everything up into hundreds of classes offers great flexibility for me to combine classes to come up with my own solution to a given problem. This approach may be verbose, but it does lead to a better understanding of the underlying algorithms involved.


import java.applet.Applet;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.net.URL;

public class RotateImage extends Applet {

private Image image;

AffineTransform identity = new AffineTransform();

private URL getURL(String filename) {
URL url = null;
try {
url = this.getClass().getResource(filename);
}
catch(Exception e){}
return url;
}

public void init() {
image = getImage(getCodeBase(), "image.jpg");
}

public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D)g;
AffineTransform trans = new AffineTransform();
trans.setTransform(identity);
trans.rotate( Math.toRadians(45) );
g2d.drawImage(image, trans, this);
}
}

This looks a lot worse than it is. There are 22 lines of code here (not counting white space and curly braces), but the majority of them are spent on importing Java libraries and loading the image. Only six of them are spent rotating and displaying the image.

The most complicated part of the code is the AffineTransform object. According to Sun's AffineTransform API, "The AffineTransform class represents a 2D affine transform that performs a linear mapping from 2D coordinates to other 2D coordinates that preserves the "straightness" and "parallelness" of lines." If you experiment a little with this class (or just continue reading the API), you'll see that it can be used not just to rotate, but also to scale, flip, and shear an image as well.

3 comments:

Anonymous said...

Hello,

I have a question regarding the affine transformation with PIL. Since an affine transformation is defined by 6 unknowns I was thinking at defining an initial triangle source and a final one. Basically I'm trying to modify a rectangular shape to fit in a quadrangle, in order to do this I've split the initial rectangle in 2 triangles.

Using the documentation an affine transformation should map a point x,y into xx,yy by:

xx=a*x+b*y+c
yy=d*x+e*y+f

Using a triangle I was able to find a,b,c,d,e,f. However it seems the PIL affine transform scales the result. The final image is about 4 times larger then what I've imposed when I've calculated a,b,c,d,e,f.

Any help will be appreciated.

Thanks,

Danny

Aleks said...

The following function will rotate a buffered image that comes in indeterminate if it is a perfect square or not.

public BufferedImage rotate(BufferedImage image)
{
/*
* Affline transform only works with perfect squares. The following
* code is used to take any rectangle image and rotate it correctly.
* To do this it chooses a center point that is half the greater
* length and tricks the library to think the image is a perfect
* square, then it does the rotation and tells the library where
* to find the correct top left point. The special cases in each
* orientation happen when the extra image that doesn't exist is
* either on the left or on top of the image being rotated. In
* both cases the point is adjusted by the difference in the
* longer side and the shorter side to get the point at the
* correct top left corner of the image. NOTE: the x and y
* axes also rotate with the image so where width > height
* the adjustments always happen on the y axis and where
* the height > width the adjustments happen on the x axis.
*
*/
AffineTransform xform = new AffineTransform();

if (image.getWidth() > image.getHeight())
{
xform.setToTranslation(0.5 * image.getWidth(), 0.5 * image.getWidth());
xform.rotate(_theta);

int diff = image.getWidth() - image.getHeight();

switch (_thetaInDegrees)
{
case 90:
xform.translate(-0.5 * image.getWidth(), -0.5 * image.getWidth() + diff);
break;
case 180:
xform.translate(-0.5 * image.getWidth(), -0.5 * image.getWidth() + diff);
break;
default:
xform.translate(-0.5 * image.getWidth(), -0.5 * image.getWidth());
break;
}
}
else if (image.getHeight() > image.getWidth())
{
xform.setToTranslation(0.5 * image.getHeight(), 0.5 * image.getHeight());
xform.rotate(_theta);

int diff = image.getHeight() - image.getWidth();

switch (_thetaInDegrees)
{
case 180:
xform.translate(-0.5 * image.getHeight() + diff, -0.5 * image.getHeight());
break;
case 270:
xform.translate(-0.5 * image.getHeight() + diff, -0.5 * image.getHeight());
break;
default:
xform.translate(-0.5 * image.getHeight(), -0.5 * image.getHeight());
break;
}
}
else
{
xform.setToTranslation(0.5 * image.getWidth(), 0.5 * image.getHeight());
xform.rotate(_theta);
xform.translate(-0.5 * image.getHeight(), -0.5 * image.getWidth());
}

AffineTransformOp op = new AffineTransformOp(xform, AffineTransformOp.TYPE_BILINEAR);

return op.filter(image, null);
}

Anonymous said...

Thanks, Aleks!