Mit der Klasse AffineTransform lassen sich graphische
Transformationen wie Rotationen, Scherungen, Translationen (=Verschiebungen) und
Skalierungen vornehmen.
Unter einer
affinen Transformation versteht man die
geometrische Transformation eines Koordinatensystems in ein anderes. Es werden
hier zwei Beispiele vorgestellt, die auf unterschiedliche Weise diese
Problematik beleuchten.
Script 1 lädt zum Experimentieren ein. Es modifiziert einfache
geometrische Grundformen (ein Rechteck und eine Linie) durch einfaches Ein- und
Auskommentieren kleiner, nummerierter Scriptbereiche.
Script 2 stellt eine kleine Applikation dar, die einige
Möglichkeiten der Klasse
AffineTransform anhand eines Bildes aufzeigt
und die Ergebnisse in einer
GUI (Graphical User Interphase =
Bildschirmfenster) präsentiert
Das erste Beispiel besteht aus zwei Klassen. Die Hauptklasse
mit der main-Methode ist von JFrame abgeleitet und stellt den
größten Teil des GUI in einem BorderLayout dar. Ins Zentrum
wird ein Objekt der zweiten Klasse als Zeichenfläche geladen, darunter befindet
sich ein JSlider, der, je nach Auswahl, eine schrittweise Transformation
ermöglicht. Der Slider ist bei einem ChangeListener angemeldet, der seinen
eingestellten Wert an eine Variable übergibt, die das gezeichnete Objekt
modifiziert und anschließend das JPanel neu zeichnet.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class AffineTransformation extends JFrame implements ChangeListener {
AffineTransformationPanel panel;
public AffineTransformation(){
panel = new AffineTransformationPanel();
setLayout(new BorderLayout());
add(panel, BorderLayout.CENTER);
JSlider slider = new JSlider(0, 10, 0);
slider.addChangeListener(this);
add(slider, BorderLayout.SOUTH);
this.setSize(300, 300);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setVisible(true);
}
public static void main(String[] args) {
new AffineTransformation();
}
public void stateChanged(ChangeEvent e) {
panel.setNum((double)((JSlider)e.getSource()).getValue()/5);
panel.repaint();
}
}
class AffineTransformationPanel extends JPanel {
double num;
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, this.getWidth(), this.getHeight());
g2d.setColor(Color.BLACK);
int x = 15;
int y = 10;
int h = 100;
int b = 200;
Rectangle2D.Double rect = new Rectangle2D.Double(x, y, b, h);
g2d.setColor(Color.RED);
g2d.draw(rect);
AffineTransform at = new AffineTransform();
// (1)Scherung und zusätzliche Translation
//at.setToShear(num, num/5);
//at.translate(num*30, num*40);
// (2)Translation (Verschiebung) und zusätzliche Scherung
//at.setToTranslation(num*30, num*40);
//at.shear(num, num/5);
// (3)Skalierung
//at.setToScale(num, num);
// (4)Rotation um den Punkt x/y
//at.setToRotation(num, x, y);
// (5)Rotation um den Punkt 0/0
//at.setToRotation(num);
Shape s = at.createTransformedShape(rect);
g2d.setColor(Color.BLACK);
g2d.draw(s);
// Verbindung der affinen Transformation mit dem gesamten Grafics-Kontext
// bewirkt eine Transformation aller nachfolgender Objekte
g2d.transform(at);
Line2D.Double line = new Line2D.Double(100, 0, 100, 300);
g2d.draw(line);
}
void setNum(double num){
this.num = num;
}
}
Diese Variable ist das einzige Feld, das die Klasse
AffineTransformationPanel definiert. In der von JPanel abgeleiteten Klasse sind
zudem noch zwei Methoden deklariert. Eine Getter-Methode für die Variable und
die überschriebene Methode paintComponent(), die die Zeichenarbeit übernimmt.
Hier wird zunächst das als Parameter übergebene Graphics-Objekt in ein Objekt
des Typs Graphics2D gecastet und mit diesem ein weißes gefülltes Rechteck in der
Größe des Panels gezeichnet, um eine weiße Grundlage zu erhalten. In der Folge
werden vier Variablen, die den linken oberen Initialisierungspunkt und die
Breite und Höhe eines Rechtecks angeben deklariert und mit den entsprechenden
Werten initialisiert. Mit diesen Werten wird ein Objekt der Klasse
Rectangle2D.Double erzeugt. Die Wandlung der int-Werte in die notwendigen
double-Werte erfolgt selbstständig. Das Objekt stellt ein als Umriss
gezeichnetes Rechteck dar, das auf dem Graphics2D-Objekt durch die Mehtode
draw() erzeugt wird. Es dient als Gegenstand der folgenden Manipulationen.
Hierzu wird als erstes ein Objekt der Klasse
AffineTransform
erzeugt. Auf ihm werden die geometrischen Transformationen vorgenommen. Einige
sind in den folgenden fünf Abschnitten dargestellt und können einzeln
auskommentiert werden, um ihre Auswirkung zu erkunden. Allen gemeinsam ist die
Tatsache, das auf dem
AffineTransform-Objekt eine Methode
setToXXX() ausgeführt
wird, wobei
XXX angibt, welche Transformation ausgeführt wird. Die vom
JSlider
belegte Variable num wird hierbei als Parameter übergeben. Eine zusätzliche
Transformation kann wie in (1) und (2) gezeigt über die Methoden
shear(),
translate(),
rotate() und
scale() durchgeführt werden. Wichtig zu wissen sind
drei Dinge:
- Bedingt durch die zugrunde liegende Matrix-Berechnung findet ohne explizite
Angabe eines Koordinatenzentrums bei der Scheerung und Skalierung immer auch
eine Translation (=Verschiebung) statt.
- Die Reihenfolge der Transformation ist bei mehreren hintereinander
geschalteten Operationen nicht gleichgültig. Durch entsprechendes Auskommentieren und
Verschieben der Abschnitte kann das Verhalten leicht überprüft werden.
- Die angesprochenen Methoden setToXXX() zeichnen noch kein transformiertes
Objekt, sondern führen nur die Transformation selbst aus.
Die Zeichnung selber wird nach Anpassung der Vordergrundfarbe im Beispiel durch die Methode
draw() vorgenommen, die auf dem
Graphics2D-Objekt ausgeführt wird.
Möchte man nicht für jedes zu transformierende Objekt eine andere
AffineTransform-Methode aufrufen, sondern verschiedene Objekte auf gleiche Weise
manipulieren, so lässt sich das
AffineTransform-Objekt auch direkt dem
Graphik-Kontext mit der Methode
transform() übergeben. Als Beispiel dient die im
letzten Abschnitt der Methode erzeugte Linie.
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridLayout;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/**
* Die Klasse demonstriert verschiedene grafische Transformationen
* anhand eines Bildes (Pfad anpassen!)
*/
public class ImageDemo extends JFrame implements ChangeListener, ActionListener {
private final String bildPfad = "test.jpg";
private BufferedImage[] img;
private JRadioButton scaleRB, rotateRB, transRB, scherRB;
private JLabel origLabel, transLabel;
private JTextField xScaleField, yScaleField,
xPosRotateField, yPosRotateField, xTransField, yTransField,
xScherField, yScherField;
private JButton button;
JSlider rotateSlider;
private CardLayout cl;
private JPanel cardPanel;
private Box scalePanel, rotatePanel, transPanel, scherPanel;
private static final long serialVersionUID = 1L;
public ImageDemo() {
JPanel rechtsPanel = initRB();
img = getImages();
if (img == null) {
System.out.println("Image-Import fehlgeschlagen!");
System.exit(1);
}
ImageIcon origIcon = new ImageIcon(img[0]);
origLabel = new JLabel(origIcon);
origLabel.setBorder(createBorder("Original"));
ImageIcon transIcon = new ImageIcon();
transLabel = new JLabel(transIcon);
transLabel.setBorder(createBorder("Transformation"));
JPanel centerPanel = new JPanel();
centerPanel.setLayout(new GridLayout(2, 1));
centerPanel.setBackground(Color.DARK_GRAY);
centerPanel.add(origLabel);
centerPanel.add(transLabel);
getContentPane().add(centerPanel, BorderLayout.CENTER);
getContentPane().add(rechtsPanel, BorderLayout.EAST);
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
setSize(dim.width, dim.height);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
}
private Border createBorder(String title) {
return new TitledBorder(new LineBorder(Color.WHITE), title,
TitledBorder.ABOVE_BOTTOM, TitledBorder.LEADING, new Font(
"Verdana", Font.PLAIN, 12), Color.WHITE);
}
private JPanel initRB() {
scaleRB = new JRadioButton("Skalierung", true);
scaleRB.addChangeListener(this);
rotateRB = new JRadioButton("Rotation");
rotateRB.addChangeListener(this);
transRB = new JRadioButton("Translation");
transRB.addChangeListener(this);
scherRB = new JRadioButton("Scherung");
scherRB.addChangeListener(this);
ButtonGroup buttonGroup = new ButtonGroup();
buttonGroup.add(scaleRB);
buttonGroup.add(rotateRB);
buttonGroup.add(transRB);
buttonGroup.add(scherRB);
JPanel rBPanel = new JPanel(new GridLayout(4, 1, 0, 5));
rBPanel.add(scaleRB);
rBPanel.add(rotateRB);
rBPanel.add(transRB);
rBPanel.add(scherRB);
JLabel xScaleLabel, yScaleLabel, faktorRotateLabel, xPosRotateLabel, yPosRotateLabel, xTransLabel, yTransLabel, xScherLabel, yScherLabel;
xScaleLabel = new JLabel("X-Wert:");
yScaleLabel = new JLabel("Y-Wert:");
faktorRotateLabel = new JLabel("Winkel");
xPosRotateLabel = new JLabel("X-Wert:");
yPosRotateLabel = new JLabel("Y-Wert:");
xTransLabel = new JLabel("X-Wert:");
yTransLabel = new JLabel("Y-Wert:");
xScherLabel = new JLabel("X-Wert:");
yScherLabel = new JLabel("Y-Wert:");
xScaleField = new JTextField(4);
xScaleField.setPreferredSize(new Dimension(100, 20));
yScaleField = new JTextField(4);
yScaleField.setPreferredSize(new Dimension(100, 20));
// Bereich 0 - 2*PI
// wird im Listener durch 100 dividiert
rotateSlider = new JSlider(0, 628, 0);
rotateSlider.setMajorTickSpacing(150);
rotateSlider.setPaintTicks(true);
xPosRotateField = new JTextField(4);
yPosRotateField = new JTextField(4);
xTransField = new JTextField(4);
yTransField = new JTextField(4);
xScherField = new JTextField(4);
yScherField = new JTextField(4);
Dimension fillDim = new Dimension(10, Short.MAX_VALUE);
scalePanel = new Box(BoxLayout.Y_AXIS);
scalePanel.add(xScaleLabel);
scalePanel.add(xScaleField);
scalePanel.add(yScaleLabel);
scalePanel.add(yScaleField);
scalePanel.add(new Box.Filler(fillDim, fillDim, fillDim));
rotatePanel = new Box(BoxLayout.Y_AXIS);
rotatePanel.add(faktorRotateLabel);
rotatePanel.add(rotateSlider);
rotatePanel.add(xPosRotateLabel);
rotatePanel.add(xPosRotateField);
rotatePanel.add(yPosRotateLabel);
rotatePanel.add(yPosRotateField);
rotatePanel.add(new Box.Filler(fillDim, fillDim, fillDim));
transPanel = new Box(BoxLayout.Y_AXIS);
transPanel.add(xTransLabel);
transPanel.add(xTransField);
transPanel.add(yTransLabel);
transPanel.add(yTransField);
transPanel.add(new Box.Filler(fillDim, fillDim, fillDim));
scherPanel = new Box(BoxLayout.Y_AXIS);
scherPanel.add(xScherLabel);
scherPanel.add(xScherField);
scherPanel.add(yScherLabel);
scherPanel.add(yScherField);
scherPanel.add(new Box.Filler(fillDim, fillDim, fillDim));
cl = new CardLayout(0, 20);
cardPanel = new JPanel(cl);
cardPanel.add(scalePanel, "Skalierung");
cardPanel.add(rotatePanel, "Rotation");
cardPanel.add(transPanel, "Translation (Verschieben)");
cardPanel.add(scherPanel, "Scherung");
button = new JButton("Transformieren");
button.addActionListener(this);
JPanel mainPanel = new JPanel(new BorderLayout(5, 10));
mainPanel.add(rBPanel, BorderLayout.NORTH);
mainPanel.add(cardPanel, BorderLayout.CENTER);
mainPanel.add(button, BorderLayout.SOUTH);
return mainPanel;
}
/**
* skaliert ein BufferedImage
*/
private BufferedImage[] scaleImage(String xStr, String yStr) {
BufferedImage[] iArr = getImages();
if (iArr == null)
return null;
// 1.0 ist Originalgroesse
double x = str2double(xStr);
double y = str2double(yStr);
if(x == 0 || y == 0) return null;
AffineTransform trans = AffineTransform.getScaleInstance(x, y);
AffineTransformOp op = new AffineTransformOp(trans,
AffineTransformOp.TYPE_BILINEAR);
op.filter(iArr[0], iArr[1]);
return iArr;
}
/**
* dreht ein BufferedImage
*/
private BufferedImage[] rotateImage(String fStr, String xStr, String yStr) {
BufferedImage[] iArr = getImages();
if (iArr == null)
return null;
double x = str2double(xStr);
double y = str2double(yStr);
if(xStr.equals("-1")) x = iArr[0].getWidth()/2;
if(yStr.equals("-1")) y = iArr[0].getHeight()/2;
// um die Mitte rotieren
// f = 6.284 - volle Drehung
double f = str2double(fStr);
AffineTransform trans = AffineTransform.getRotateInstance(f, x, y);
AffineTransformOp op = new AffineTransformOp(trans,
AffineTransformOp.TYPE_BILINEAR);
op.filter(iArr[0], iArr[1]);
return iArr;
}
/**
* transformiert den Ursprungspunkt eines BufferedImage
*/
private BufferedImage[] transImage(String xStr, String yStr) {
BufferedImage[] iArr = getImages();
if (iArr == null)
return null;
// Translation
double x = str2double(xStr);
double y = str2double(yStr);
AffineTransform trans = AffineTransform.getTranslateInstance(x, y);
AffineTransformOp op = new AffineTransformOp(trans,
AffineTransformOp.TYPE_BILINEAR);
op.filter(iArr[0], iArr[1]);
return iArr;
}
/**
* schert ein BufferedImage
*/
private BufferedImage[] scherImage(String xStr, String yStr) {
BufferedImage[] iArr = getImages();
if (iArr == null)
return null;
// scheren
double x = str2double(xStr);
double y = str2double(yStr);
AffineTransform trans = AffineTransform.getShearInstance(x, y);
AffineTransformOp op = new AffineTransformOp(trans,
AffineTransformOp.TYPE_BILINEAR);
op.filter(iArr[0], iArr[1]);
return iArr;
}
/**
*
* @param fileName
* String des Originalbildes
* @return BufferedImage[] Array mit zwei BufferdImages: das erste ist das
* Original-Bild, das zweite ist das Zielbild für die
* Transformation
*/
private BufferedImage[] getImages() {
File file = new File(bildPfad);
BufferedImage origImg = null;
try {
origImg = ImageIO.read(file);
} catch (IOException ex) {
System.err.println("Image-Datei kann nicht gelesen werden!");
System.exit(1);
}
if (origImg != null) {
BufferedImage destImg = new BufferedImage(origImg.getWidth(),
origImg.getHeight(), origImg.getType());
BufferedImage[] i = { origImg, destImg };
return i;
}
return null;
}
private void setImg(BufferedImage[] images) {
if (images == null) {
System.out.println("fehlgeschlaagen");
return;
}
ImageIcon origIcon = new ImageIcon(images[0]);
ImageIcon transIcon = new ImageIcon(images[1]);
origLabel.setIcon(origIcon);
transLabel.setIcon(transIcon);
}
private double str2double(String dStr) {
if (dStr == null)
return 1;
dStr = dStr.replace(',', '.');
try {
return new Double(dStr).doubleValue();
} catch (NumberFormatException nfe) {
System.out.println("falsches Zahlformat");
}
return 0;
}
public void stateChanged(ChangeEvent e) {
if (e.getSource() == scaleRB) {
cl.show(cardPanel, "Skalierung");
}
if (e.getSource() == rotateRB) {
cl.show(cardPanel, "Rotation");
}
if (e.getSource() == transRB) {
cl.show(cardPanel, "Translation (Verschieben)");
}
if (e.getSource() == scherRB) {
cl.show(cardPanel, "Scherung");
}
}
public void actionPerformed(ActionEvent e) {
if (scaleRB.isSelected()) {
if(xScaleField.getText().equals("") || yScaleField.getText().equals("")) return;
img = scaleImage(xScaleField.getText(), yScaleField.getText());
setImg(img);
}
if (rotateRB.isSelected()) {
String xFaktor = "-1", yFaktor = "-1";
if(!xPosRotateField.getText().equals("") && !yPosRotateField.getText().equals("")){
xFaktor = xPosRotateField.getText();
yFaktor = yPosRotateField.getText();
}
double faktor = (double)rotateSlider.getValue()/100;
img = rotateImage(new Double(faktor).toString(), xFaktor, yFaktor);
setImg(img);
}
if (transRB.isSelected()) {
if(xTransField.getText().equals("") || yTransField.getText().equals("")) return;
img = transImage(xTransField.getText(), yTransField.getText());
setImg(img);
}
if (scherRB.isSelected()) {
if(xScherField.getText().equals("") || yScherField.getText().equals("")) return;
img = scherImage(xScherField.getText(), yTransField.getText());
setImg(img);
}
}
public static void main(String args[]) {
new ImageDemo();
}
}