Rotating Image with Affinetransform

Rotating Image with AffineTransform

The major problem (that I can see) is the translation of the Graphics context which is offset the position that the rotation will take place.

I "think" rotation by default occurs at the top/left corner of the Graphics context (where it's 0x0 position is, which you've translated to something else), this could be causing the image to be rotated out of frame (or viewable area)

You should provide a "anchor" point where the rotation takes place, typically, the centre is my personal preference.

The following example simply has a master image (due to size constraints I had to scale it, but you may not need this). I then use this to generate a "rotated" instance which is sized to allow the image to fit within in. This is a lot of fun with trig - I stole the code from somewhere, so credit to that developer.

The example allows you to click any where and it will change the rotation pivot, so you can see what's going on. The default position is the centre of the pane...

Spinning

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class SampleRotation {

public static void main(String[] args) {
new SampleRotation();
}

public SampleRotation() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}

final RotationPane rotationPane = new RotationPane();
final JSlider slider = new JSlider(0, 100);
slider.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
double angle = 720d * (slider.getValue() / 100d);
rotationPane.setAngle(angle);
}
});
slider.setValue(0);

JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(rotationPane);
frame.add(slider, BorderLayout.SOUTH);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}

public class RotationPane extends JPanel {

private BufferedImage img;
private BufferedImage rotated;
private double angle;
private Point clickPoint;

public RotationPane() {
try {
img = ImageIO.read(new File("/Users/swhitehead/Dropbox/MegaTokyo/issue459.jpg"));
BufferedImage scaled = new BufferedImage(img.getWidth() / 2, img.getHeight() / 2, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = scaled.createGraphics();
g2d.setTransform(AffineTransform.getScaleInstance(0.5d, 0.5d));
g2d.drawImage(img, 0, 0, this);
g2d.dispose();
img = scaled;
setAngle(0d);
} catch (IOException ex) {
ex.printStackTrace();
}

addMouseListener(new MouseAdapter() {

@Override
public void mouseClicked(MouseEvent e) {
clickPoint = e.getPoint();
repaint();
}

});

}

public void setAngle(double angle) {
this.angle = angle;

double rads = Math.toRadians(getAngle());
double sin = Math.abs(Math.sin(rads)), cos = Math.abs(Math.cos(rads));
int w = img.getWidth();
int h = img.getHeight();
int newWidth = (int) Math.floor(w * cos + h * sin);
int newHeight = (int) Math.floor(h * cos + w * sin);

rotated = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = rotated.createGraphics();
AffineTransform at = new AffineTransform();
at.translate((newWidth - w) / 2, (newHeight - h) / 2);

int x = clickPoint == null ? w / 2 : clickPoint.x;
int y = clickPoint == null ? h / 2 : clickPoint.y;

at.rotate(rads, x, y);
g2d.setTransform(at);
g2d.drawImage(img, 0, 0, this);
g2d.setColor(Color.RED);
g2d.drawRect(0, 0, newWidth - 1, newHeight - 1);
g2d.dispose();

repaint();
}

public double getAngle() {
return angle;
}

@Override
public Dimension getPreferredSize() {
return img == null ? new Dimension(200, 200) : new Dimension(img.getWidth(this), img.getHeight(this));
}

@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (rotated != null) {
Graphics2D g2d = (Graphics2D) g.create();

int x = (getWidth() - rotated.getWidth()) / 2;
int y = (getHeight() - rotated.getHeight()) / 2;
g2d.drawImage(rotated, x, y, this);

g2d.setColor(Color.RED);

x = clickPoint == null ? getWidth() / 2 : clickPoint.x;
y = clickPoint == null ? getHeight()/ 2 : clickPoint.y;

x -= 5;
y -= 5;

g2d.drawOval(x, y, 10, 10);
g2d.dispose();
}
}
}
}

Java Graphics2D AffineTransform Image Rotation

I've transformed your code using the recommendation of MadProgrammer:

  • Don't override paint, override paintComponent.
  • Call super.paint before performing any custom painting,
  • Never call finalize on anything and especially not on objects you didn't create yourself.
  • Use a Swing Timer

Note the following

  • A qualified this is used to access the ImageRotationView instance from the ActionListener inner class.

  • AffineTransform.getRotateInstance returns a transform that rotates coordinates around an anchor point.

  • Speed can be optimized but it works correctly like this.
  • This class works as a standalone application
  • A file named dice.png should be present in the base directory.

.

import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.Timer;

public class ImageRotationFrame {

public static void main(String[] args) {
new ImageRotationFrame();
}

public ImageRotationFrame() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame("Testing");
frame.setSize(400, 400);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new ImageRotationComponent());
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});

}

private class ImageRotationComponent extends JComponent {

Image arrow;
double angle = 0.0;

@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
angle += 0.4;
AffineTransform trans = AffineTransform.getRotateInstance(angle, getWidth() / 2, getHeight() / 2);
((Graphics2D) g).drawImage(arrow, trans, this);
}

public ImageRotationComponent() {
try {
arrow = ImageIO.read(new File("dice.png"));
} catch (IOException e) {
e.printStackTrace();
}
int delay = 500; //milliseconds
ActionListener taskPerformer = new ActionListener() {
@Override
public void actionPerformed(ActionEvent evt) {
ImageRotationComponent.this.repaint();
}
};
new Timer(delay, taskPerformer).start();
}
}
}

Rotating Image with AffineTransform Produces Visually Correct Pixels, but getRGB Doesn't Match

You are looking at the wrong pixel.

Assume your image is 1x1 pixel in size. You're originally looking at pixel (0, 0). Then with your code, in the destination image, you're looking at pixel (width - 0, height - 1), which turns out to be pixel (1, 1). When clearly you still need to be looking at (0, 0) as the image doesn't have a pixel at (1, 1).

In your case, your transformed coordinate isn't outside your image, but it points to the wrong pixel.

To fix your code, change the lines that calculate your expX and expY values, and subtract 1 from each:

int expX = destImage.getWidth() - origX - 1;
int expY = destImage.getHeight() - origY - 1;

That returns the correct pixel values when I run your code (on my own image, as you haven't provided the image that you used).

There is another issue in the code that will only surface with an odd-numbered width or height: you're using integer arithmetic to determine the center of rotation, which rounds down when it shouldn't. Change the lines that determine the center to (to use floating-point numbers, so that you can get a fractional number as a result) :

double centerX = origImage.getWidth() / 2.0;
double centerY = origImage.getHeight() / 2.0;

AffineTransform.rotate() - how do I xlate, rotate, and scale at the same time?

Okay, this is a little slight of hand. The example code will only work for 90 degree increments (it was only designed this way), to do smaller increments you to use some trig to calculate the image width and height (there's a answer somewhere for that to ;))

public class ImagePane extends JPanel {

private BufferedImage masterImage;
private BufferedImage renderedImage;

public ImagePane(BufferedImage image) {
masterImage = image;
applyRotation(0);
}

@Override
public Dimension getPreferredSize() {
return new Dimension(renderedImage.getWidth(), renderedImage.getHeight());
}

@Override
public Dimension getMinimumSize() {
return getPreferredSize();
}

protected int getVirtualAngle(int angle) {
float fRotations = (float) angle / 360f;
int rotations = (int) (fRotations - (fRotations / 1000));

int virtual = angle - (rotations * 360);

if (virtual < 0) {
virtual = 360 + virtual;
}

return virtual;
}

public void applyRotation(int angle) {
// This will only work for angles of 90 degrees...

// Normalize the angle to make sure it's only between 0-360 degrees
int virtualAngle = getVirtualAngle(angle);
Dimension size = new Dimension(masterImage.getWidth(), masterImage.getHeight());
int masterWidth = masterImage.getWidth();
int masterHeight = masterImage.getHeight();

double x = 0; //masterWidth / 2.0;
double y = 0; //masterHeight / 2.0;

switch (virtualAngle) {
case 0:
break;
case 180:
break;
case 90:
case 270:
size = new Dimension(masterImage.getHeight(), masterImage.getWidth());
x = (masterHeight - masterWidth) / 2.0;
y = (masterWidth - masterHeight) / 2.0;
break;
}
renderedImage = new BufferedImage(size.width, size.height, masterImage.getTransparency());
Graphics2D g2d = renderedImage.createGraphics();

AffineTransform at = AffineTransform.getTranslateInstance(x, y);

at.rotate(Math.toRadians(virtualAngle), masterWidth / 2.0, masterHeight / 2.0);
g2d.drawImage(masterImage, at, null);

g2d.dispose();
}

@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);

Graphics2D g2d = (Graphics2D) g;
int width = getWidth() - 1;
int height = getHeight() - 1;

int x = (width - renderedImage.getWidth()) / 2;
int y = (height - renderedImage.getHeight()) / 2;

g2d.drawImage(renderedImage, x, y, this);
}

}

Now, you could simply "flip" the image vertically, if that works better for you

public class FlipPane extends JPanel {

private BufferedImage masterImage;
private BufferedImage renderedImage;

public FlipPane(BufferedImage image) {
masterImage = image;
flipMaster();
}

@Override
public Dimension getPreferredSize() {
return new Dimension(renderedImage.getWidth(), renderedImage.getHeight());
}

@Override
public Dimension getMinimumSize() {
return getPreferredSize();
}

protected void flipMaster() {
renderedImage = new BufferedImage(masterImage.getWidth(), masterImage.getHeight(), masterImage.getTransparency());
Graphics2D g2d = renderedImage.createGraphics();
g2d.setTransform(AffineTransform.getScaleInstance(1, -1));
g2d.drawImage(masterImage, 0, -masterImage.getHeight(), this);
g2d.dispose();
}

@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);

Graphics2D g2d = (Graphics2D) g;
int width = getWidth() - 1;
int height = getHeight() - 1;

int x = (width - renderedImage.getWidth()) / 2;
int y = (height - renderedImage.getHeight()) / 2;

g2d.drawImage(renderedImage, x, y, this);
}
}

This basically results in:

Image rotation example

Original | 180 degree rotation | Vertical inversion...

Now, if you change the flipMaster method to read:

g2d.setTransform(AffineTransform.getScaleInstance(-1, -1));
g2d.drawImage(masterImage, -masterImage.getWidth(), -masterImage.getHeight(), this);

You'll get the same effect as the 180 rotation ;)

I'm rotating image in java but want to save rotated image

Let's start with, this isn't rotating the image, it's rotating the Graphics context which is used to display the image, it doesn't affect the original image at all

@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);

Graphics2D g2d = (Graphics2D) g.create();

g2d.setColor(Color.RED);
g2d.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight());
g2d.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2);

g2d.setColor(Color.BLACK);
int x = (getWidth() - image.getWidth()) / 2;
int y = (getHeight() - image.getHeight()) / 2;
AffineTransform at = new AffineTransform();
at.setToRotation(getAngle(), x + (image.getWidth() / 2), y + (image.getHeight() / 2));
at.translate(x, y);
g2d.setTransform(at);
g2d.drawImage(image, 0, 0, this);
g2d.dispose();
}

And then this...

  try {
File imagefile = new File("C:/pics/1206.jpg");
image = ImageIO.read(imagefile);

ImageIO.write(image, "jpg",new File("C:/pics"));
ImageIO.write(image, "bmp",new File("C:/pics"));
ImageIO.write(image, "gif",new File("C:/picsf"));
ImageIO.write(image, "png",new File("C:/pics"));
} catch (IOException ex) {
ex.printStackTrace();
}

which just saves the original image to a number of different formats ... to the same file , but doesn't even react to any changes to the angle

Instead, you need to use something which generates a new image from the original, rotated by the amount you want...

  public BufferedImage rotateImageByDegrees(BufferedImage img, double degrees) {
double rads = Math.toRadians(degrees);
double sin = Math.abs(Math.sin(rads)), cos = Math.abs(Math.cos(rads));
int w = img.getWidth();
int h = img.getHeight();
int newWidth = (int) Math.floor(w * cos + h * sin);
int newHeight = (int) Math.floor(h * cos + w * sin);

BufferedImage rotated = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = rotated.createGraphics();
AffineTransform at = new AffineTransform();
at.translate((newWidth - w) / 2, (newHeight - h) / 2);

int x = clickPoint == null ? w / 2 : clickPoint.x;
int y = clickPoint == null ? h / 2 : clickPoint.y;

at.rotate(rads, x, y);
g2d.setTransform(at);
g2d.drawImage(img, 0, 0, this);
g2d.setColor(Color.RED);
g2d.drawRect(0, 0, newWidth - 1, newHeight - 1);
g2d.dispose();

return rotated;
}

Then you can save it...

File imagefile = new File("C:/pics/1206.jpg");
image = ImageIO.read(imagefile);

BufferedImage rotated = rotateImageByDegrees(image, 22.5);
ImageIO.write(rotated, "png", new File("RotatedBy225.png"));

So, the next time you use one of my previous examples and I tell you it's not doing what you think/want it to, I hope you will understand my meaning better and look more closly at the other examples we show you

Java image rotation with AffineTransform outputs black image, but works well when resized

Passing a new BufferedImage into the filter() method rather than letting it create its own works (not completely black).

Also the transform did not appear to work correctly, the image ended up being offset in the destination. I was able to fix it by manually applying the necessary translations, note these work in reverse order, and in the destination image the width = the old height, and height = the old width.

AffineTransform tx = new AffineTransform();

// last, width = height and height = width :)
tx.translate(originalImage.getHeight() / 2,originalImage.getWidth() / 2);
tx.rotate(Math.PI / 2);
// first - center image at the origin so rotate works OK
tx.translate(-originalImage.getWidth() / 2,-originalImage.getHeight() / 2);

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

// new destination image where height = width and width = height.
BufferedImage newImage =new BufferedImage(originalImage.getHeight(), originalImage.getWidth(), originalImage.getType());
op.filter(originalImage, newImage);

The javadoc for filter() states that it will create a BufferedImage for you, I'm still unsure why this does not work, there must be an issue here.

 If the destination image is null, a BufferedImage is created with the source ColorModel.


Related Topics



Leave a reply



Submit