Image eines ImageIcons rotieren


#1

Hey zusammen.

Ich programmiere gerade ein kleines Bilddarstellungsprogramm (Siehe letzten Thread) und man kann das Bild auch korrekterweise verschieben, mit Mausrad zoomen und per Filter filtern. Nun möchte ich das ganze aber noch drehen können und hab mir dadurch per Google anreize geholt. Es kam raus in der Pc das zu rotieren entweder per affiliateTransform(geht nicht) oder per graphics selber. UNd letzteres funktioniert auch, aber… ich zeig euch mal 2 Beispielscreens:
Wenn Rotieren drin ist (hardcoded auf 1/4PI):

Wenn die Zeile auskommentiert ist:

ALso das Bild wandert nach unten links… Da auch das verschieben jetz in dem WInkel schief verläuft etc liegt es sehr wahrscheinlich daran, das die ganze COmponent gedreht wird. Wie schaffe ich es aber nun, nur das Bild zu drehen? Von einem ImageIcon kriegt ich ja keine getGraphics() zurück. Die Pc meines ImageComponents schaut so aus, falls von Belang:

@Override
    protected void paintComponent(Graphics gr) {
        super.paintComponent(gr);
        if (imageIcon != null) {
            computeCurrWAndH();
            Container parent = getParent();
            while (parent.getClass() != JFrame.class) {
                parent = parent.getParent();
            }
            if (firstDraw) {
                currX = (parent.getWidth() - currW) / 2;
            }

            if (firstDraw) {
                currY = (parent.getHeight() - currH) / 2;
            }
            setPreferredSize(new Dimension(currW, currH));
            Graphics2D g = (Graphics2D) gr;
//                    g.rotate(Math.PI / 4);
                g.drawImage(imageIcon.getImage(), currX, currY - controlH, currW, currH, this);
                firstDraw = false;
            }
        }

Wie löse ich das nun? BufferedImage kommt nicht in Frage, da das mein komplettes Restprogramm verhunzen würde (alleine schon animierte gifs)


#2

Beim schnellen Überfliegen: Das Bild “wandert” nicht wirklich. Es wird nur um einen anderen Punkt rotiert, als den Mittelpunkt. Die Lösung sollte mit https://docs.oracle.com/javase/8/docs/api/java/awt/Graphics2D.html#rotate-double-double-double- möglich sein.

Da das Bild ja nicht bei (0,0) gezeichnet wird, sondern zentriert, muss man da ggf. nochmal schauen, aber grundsätzlich sollte es mit

g.rotate(Math.PI / 4, getWidth() * 0.5, getHeight() * 0.5);

schon gehen.

WICHTIG: Das was du da mit dem getParent machst, sieht SEHR wackelig aus - wozu soll das gut sein?


#3

Damit will ich einfach die Breite vom JFrame bekommen, damit das Bild immer zentriert vom Frame gezeichnet wird, was auch funktioniert.

Aber das würde theoretisch ja nur das Problem lösen, das das Bild weiter im sichtbaren liegt, aber nicht das dann die ganze Component und nciht nur das Image gedreht wird. Dadurch klappen meine ganzen anderen Berechnungen was das verschieben des Bildes angeht nichtmehr und ich müsste an den Stellen jeweils auch die Drehwinkel mitangeben… wäre recht aufwendig und iwie auch unschöner, als einfach nur das Bild zu drehen und gut is. ODer aber ich pack mein IMagecomponent in einen Weiteren Container und lass das rotierte Bild im Imagecomponent und lass die berechnungen auf dem äußeren COntainer erfolgen… schwierig


#4

Naja, die Berechnungen machen so vermutlich nicht viel Sinn. Wenn die “Kontroll-Leiste” mit den Buttons größer wäre, würde das wohl mehr auffallen. Ganz allgemein: Die Größe des Frames sagt ja nichts (bzw. wenig) darüber aus, wie groß irgendeine Component ist, die IN dem Frame liegt.
(Oder anders: Du willst ja nicht zentriert im Frame zeichnen, sondern zentriert in der ImageComponent).
Kurz: Für sowas solle eigentlich direkt die Größe der Component verwendet werden, die das Bild zeichnet.

Was du meinst, wenn du sagst, dass

die ganze Component und nciht nur das Image gedreht wird.

leuchtet mir nicht ganz ein…

(Nochmal WICHTIG: Das “firstDraw” ist mit ziemlicher Sicherheit auch Unfug - was passiert denn, wenn du die Fenstergröße änderst?)


#5

Mit dem Problem hatte ich auch mal zu tun. Nach einem Fund im Netz (http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/java_swing_zeichenflaeche_ermitteln) habe ich folgende Klasse dazu erstellt:

package ...;

import java.awt.Container;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;

/**
 * Diese Klasse berechnet von einem Container (also z.B. einem JPanel) den wirklich zum Bemalen zur
 * Verfügung stehenden Bereich und stellt verschiedenste Methoden zur Verfügung, diesen zu
 * erfragen.
 */

public class PaintableAreaCalculator {

    /** Breite des bemalbaren Bereichs. */
    private final int paintableWidth;

    /** Höhe des bemalbaren Bereichs. */
    private final int paintableHeight;


    /** x-Koordinate der oberen, linken Ecke des bemalbaren Bereichs. */
    private final int upperLeftX;

    /** y-Koordinate der oberen, linken Ecke des bemalbaren Bereichs. */
    private final int upperLeftY;

    /** Die obere, linke Ecke. */
    private final Point upperLeftPoint;


    /** x-Koordinate der unteren, rechten Ecke des bemalbaren Bereichs. */
    private final int lowerRightX;

    /** y-Koordinate der unteren, rechten Ecke des bemalbaren Bereichs. */
    private final int lowerRightY;

    /** Die untere, rechte Ecke. */
    private final Point lowerRightPoint;


    /** Bemalbarer Bereich als Rechteck. */
    private final Rectangle rectangle;

    /**
     * Konstruktor.
     *
     * Die wirklich Zeichenfläche wird ermittelt nach:
     * http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/java_swing_zeichenflaeche_ermitteln
     *
     * @param container
     *            Zu untersuchendes Gui-Element.
     */
    public PaintableAreaCalculator(Container container) {
        /* Gesamtmaße der Komponente bestimmen: */
        int totalPanelWidth = container.getWidth();
        int totalPanelHeight = container.getHeight();

        /* Größe des bemalbaren Bereiches ermitteln: */
        Insets insets = container.getInsets();
        paintableWidth = totalPanelWidth - insets.left - insets.right - 1;
        paintableHeight = totalPanelHeight - insets.top - insets.bottom - 1;

        /* Obere linke Ecke des bemalbaren Bereiches bestimmen: */
        upperLeftX = insets.left;
        upperLeftY = insets.top;
        upperLeftPoint = new Point(upperLeftX, upperLeftY);

        /* Untere rechte Ecke des bemalbaren Bereiches bestimmen: */
        lowerRightX = totalPanelWidth - insets.right - 1;
        lowerRightY = totalPanelHeight - insets.bottom - 1;
        lowerRightPoint = new Point(lowerRightX, lowerRightY);

        /* Rechteck bestimmen: */
        rectangle = new Rectangle(upperLeftX, upperLeftY, paintableWidth, paintableHeight);
    }

    /** Getter für die Breite des bemalbaren Bereichs. */
    public int getPaintableWidth() {
        return paintableWidth;
    }

    /** Getter für die Höhe des bemalbaren Bereichs. */
    public int getPaintableHeight() {
        return paintableHeight;
    }

    /** Getter für die x-Koordinate der oberen, linken Ecke des bemalbaren Bereichs. */
    public int getUpperLeftX() {
        return upperLeftX;
    }

    /** Getter für die y-Koordinate der oberen, linken Ecke des bemalbaren Bereichs. */
    public int getUpperLeftY() {
        return upperLeftY;
    }

    /** Getter für die obere, linke Ecke. */
    public Point getUpperLeftPoint() {
        return upperLeftPoint;
    }

    /** Getter für die x-Koordinate der unteren, rechten Ecke des bemalbaren Bereichs. */
    public int getLowerRightX() {
        return lowerRightX;
    }

    /** Getter für die y-Koordinate der unteren, rechten Ecke des bemalbaren Bereichs. */
    public int getLowerRightY() {
        return lowerRightY;
    }

    /** Getter für die untere, rechte Ecke. */
    public Point getLowerRightPoint() {
        return lowerRightPoint;
    }

    /** Getter für den bemalbaren Bereich als Rechteck. */
    public Rectangle getRectangle() {
        return rectangle;
    }

}

Ja man kann und sollte das noch refakutieren (Überschriften für Teilabschnitte in Methoden schreien ja geradezu danach), aber ich hoffe, es nutzt dir vielleicht trotzdem was.


#6

Das “firstDraw” wird nur einmal ausgeführt und zwar dann, wenn das Bild zum ersten mal gezeichnet wird. Alles was danach kommt (Fenstergröße änder, zoomen, verschieben etc) machen andere Methoden,d ie die jeweiligen Variablen ins Component rein setzen. Das ist lediglich fürs erstmalige zeichnen.

Und ich will das schon zentriert im Frame haben, darum, hole ich mir ja auch die Größen vom Frame.

Und nochmal zum rotate:
Wenn ich das so einfach mache, wird die ganze ImageComponent gedereht und nicht das Bild selbst. Dadurch gehen meine ganzen anderen Berechnungen, was das Verschieben angeht kaputt, da diese das Bild im Component verschieben und wenn es z.B. um 90° gedreht ist und ich nach rechts ziehe, es nach unten geht, da es so gedreht wurde… Daher würde ich ganz einfach das Image drehen. Würde in der Theorie auch ganz einfach gehen, würde einfach createGraphics() auf dem Image des ImageIcons funktionieren… (Warum geht es da eigentlich nicht? Hab das noch nie verstanden)


#7

Ah, ich vermute, das currX und currY wird durch Mausdrags verändert?!

Falls dem so ist, muss man einfach nur die Verschiebung und die Drehung getrennt speichern. Der Effekt, den du beobachtest, ist dann schließlich nur eine Konsequenz der Tatsache, dass die Reihenfolge von “Verschieben” und “Drehen” nicht stimmt.

Ich bastel’ mal schnell ein bißchen Code…


#8

Ganz genau. currX und Y werden über nen Listener in der mousedragged verändert bzw über nen MouseWheellistener zum zoomen.

Kann dir auch die komplette Klasse von mir zur Verfügung stellen, aber da ist viel Code schnell hingeklatscht, was auf jedenfall noch verschönerbar ist :smiley:


#9

Ja, hingeklatscht ist das Stichwort :smiley:

Wie gesagt, bei sowas bietet es sich an, die Transformationen, die man machen will (Translation, Rotation, Skalierung) getrennt zu betrachten und wie gewünscht zusammenzubauen, sonst kommt man schnell in die Koordinatensystem-Hölle (oder “noch schneller”).

Hier ist mal ein Beispiel… (hingeklatscht)

package bytewelt;

import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;

import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;


public class BilderForumExtended
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(() -> createAndShowGui());
    }
    
    private static void createAndShowGui()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        TransformableImageComponent t = new TransformableImageComponent();
        
        MouseControl mouseControl = new MouseControl(t);
        t.addMouseListener(mouseControl);
        t.addMouseMotionListener(mouseControl);
        t.addMouseWheelListener(mouseControl);
        
        String path = "screenshotBoxAnimated.gif";
        ImageIcon imageIcon = new ImageIcon(path);
        t.setImageIcon(imageIcon);
        
        f.getContentPane().add(t);
        f.setSize(400,400);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }
    
    static class MouseControl extends MouseAdapter
    {
        private Point previousPoint = null;
        
        private final TransformableImageComponent transformableImageComponent;
        
        MouseControl(TransformableImageComponent transformableImageComponent)
        {
            this.transformableImageComponent = transformableImageComponent;
        }
        
        @Override
        public void mousePressed(MouseEvent e)
        {
            previousPoint = e.getPoint();
        }
        
        @Override
        public void mouseDragged(MouseEvent e)
        {
            int dx = e.getX() - previousPoint.x;
            int dy = e.getY() - previousPoint.y;
            if ((e.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0)
            {
                transformableImageComponent.translate(dx, dy);
            }
            else
            {
                double deltaAngleRad = dy / 100.0;
                transformableImageComponent.rotate(deltaAngleRad);
            }
            previousPoint = e.getPoint();
        }
        
        @Override
        public void mouseWheelMoved(MouseWheelEvent e)
        {
            double factor = 1.0 + e.getWheelRotation() * 0.1;
            transformableImageComponent.scale(factor);
        }
    }

    private static class TransformableImageComponent extends JComponent
    {
        private ImageIcon imageIcon;
        
        private Point translation = null;
        private double rotationAngleRad = 0.0;
        private double scale = 0.0;

        public void setImageIcon(ImageIcon imageIcon)
        {
            this.imageIcon = imageIcon;
            this.translation = null;
            validateTransform();
            repaint();
        }
        
        void setTranslation(Point p)
        {
            this.translation = new Point(p);
            repaint();
        }
        
        void translate(int dx, int dy)
        {
            if (translation == null)
            {
                translation = new Point();
            }
            translation.x += dx;
            translation.y += dy;
        }
        
        void setRotation(double rotationAngleRad)
        {
            this.rotationAngleRad = rotationAngleRad;
            repaint();
        }
        
        void rotate(double deltaAngleRad)
        {
            this.rotationAngleRad += deltaAngleRad;
            repaint();
        }
        
        void setScale(double scale)
        {
            this.scale = scale;
            repaint();
        }
        
        void scale(double factor)
        {
            scale *= factor;
            repaint();
        }
        
        private void validateTransform()
        {
            if (getWidth() <= 0 || getHeight() <= 0)
            {
                return;
            }
            if (imageIcon == null)
            {
                return;
            }
            if (translation != null)
            {
                return;
            }
            int iw = imageIcon.getIconWidth();
            int ih = imageIcon.getIconHeight();
            double factorW = (double) getWidth() / iw;
            double factorH = (double) getHeight() / ih;
            scale = Math.min(factorW, factorH);
            int x = (getWidth() - iw) / 2;
            int y = (getHeight() - iw) / 2;
            translation = new Point(x, y);
        }

        @Override
        protected void paintComponent(Graphics gr)
        {
            super.paintComponent(gr);
            if (imageIcon == null)
            {
                return;
            }
            Graphics2D g = (Graphics2D)gr;

            validateTransform();

            double centerX = imageIcon.getIconWidth() * 0.5; 
            double centerY = imageIcon.getIconHeight() * 0.5;

            AffineTransform at = new AffineTransform();
            System.out.println("trans "+translation);
            at.concatenate(AffineTransform.getTranslateInstance(translation.getX(), translation.getY()));
            at.concatenate(AffineTransform.getTranslateInstance(centerX, centerY));
            at.concatenate(AffineTransform.getRotateInstance(rotationAngleRad));
            at.concatenate(AffineTransform.getScaleInstance(scale, scale));
            at.concatenate(AffineTransform.getTranslateInstance(-centerX, -centerY));
            g.transform(at);
            g.drawImage(imageIcon.getImage(), 0, 0, this);
        }
    }
}

Bonuspunkte gäbe as natürlich, wenn man immer da hin zoomen würde, wo die Maus gerade ist, aber das wäre auch nicht sooo schwierig…


Koordinatensystem drehen und neue Punkte berechnen
#10

Etwas Ähnliches hab ich doch hier schon gefragt: Bild um 22,5 Grad drehen .

Und “damals” hat sogar Marco13 darauf geantwortet.

Das Bild ist jetzt abhanden-gekommen, aber hier ist es nochmal:

((Kopieren, Einfügen und autom. Hochladen funktioniert nicht mehr…))

Diesesmal nur eine Drehung um 11,25 Grad.

Probleme waren dabei:

  • (“Inverse”) :wink:
  • Welche Größe hat das neue Image?
  • Welche Position hat erster Pixel?

Edit: Inzwischen hinbekommen (Größe BufferedImage), aber mit Youtube komm ich nicht so sehr klar (Bild zuschneiden?), also Star werd ich wohl nicht:


#11

Nur um eine Sache richtig zu stellen, du drehst nicht das gesamte Component.
Wenn du Graphics2D.rotate() schreibst, dann werden nur alle darauffolgend gezeichneten Grafiken rotiert auf dein Component gezeichnet - das ist schon richtig so.

Es liegt, wie Marco13 oben angemerkt hat, wirklich einfach nur an der Reihenfolge der Transformationen.
Wenn du etwas drehst, dann drehst du nicht das Objekt um Winkel alpha, sondern du drehst das Koordinatensystem um den Winkel -alpha. Das gleiche gilt für Translation und Skalierung.
Beim Rendern fängt dann aber nicht an sich dein Bildschirm zu drehen oder zu bewegen, sondern der dargestellte Inhalt.
Und klar, wenn du das komplette Koordinatensystem erst drehst und danach verschiebst, dann verschiebst du dich in die gedrehte Richtung.
Witzig wird es erst wenn du realisierst, dass in der lustigen Welt der Matrizen, Rechnungen von rechts nach links gelesen werden und die Reihenfolge auch nicht egal ist.
Sprich A * B =/= B * A

Oder wie es Marco13 schön ausgedrückt hatte: Willkommen in der Hölle


#12

Ja, über die Reihenfolge kann man immer mal stolpern. (Da hatte ich in https://stackoverflow.com/a/38425832/3182664 ein bißchen was geschrieben).

Eigentlich ist es (speziell in 2D, und wenn alles an einer Stelle gemacht wird) recht einfach: Man schreibt seine Transformationen “rückwärts” auf, und gut.

Aber ich muss zugeben, dass ich beim verdengeln der AffineTransforms für das Beispiel oben doch drüber gestolpert bin, und erst

//                                                          v hier  und      v hier 
at.concatenate(AffineTransform.getTranslateInstance(centerX * scale, centerY * scale));
at.concatenate(AffineTransform.getScaleInstance(scale, scale));
at.concatenate(AffineTransform.getTranslateInstance(-centerX, -centerY)

mit dem scale-Faktor multipliziert hatte, weil ich kurz dachte, der müßte da mit rein :blush: (Nachher sieht’s dann immer so einfach und nahe liegend aus, dass man sich denkt: “Is’ ja klar, so hätt’ ich das (auch) sofort hingeschrieben” :wink: )


#13

:sweat::disappointed_relieved: “Hey es wäre doch ne Klasse Idee, wenn man zusätzlich mit Rechtsklick das Bild drehen könnte, ist bestimmt nich viel extraarbeit” Jaaaaa… Ich guck mir das bis morgen nochmal an, auch wenn das wohl mehr als nur paar Zeilen Code sein wird was da geändert werden muss :smiley: Problem is halt auch, wie ändere ich die Reihenfolge, da die neuen Koordinaten vom Bild ja erst beim zeichnen zum tragen kommen und da muss es ja schon gedreht sein :confused:

Hab auch schon probiert, das Imagecompnent in ein extra JPanel zu machen, die Imagecomponent ansich zu drehen und das JPanel dann zu verschieben, aber da wurde mir auf einmal das Bild nichmal mehr angezeigt^^ Muss wohl auch anders gehen


#14

Drehen ist in dem oben geposteten Beispiel schon mit drin!?

Problem is halt auch, wie ändere ich die Reihenfolge, da die neuen Koordinaten vom Bild ja erst beim zeichnen zum tragen kommen und da muss es ja schon gedreht sein

Kapier’ ich nicht. Das Drehen wird ja vermutlich immer um den Mittelpunkt passieren.

das Imagecompnent in ein extra JPanel zu machen, die Imagecomponent ansich zu drehen und das JPanel dann zu verschieben,

Wie TMII auch schon schrieb: Es ist nicht direkt so, dass dort Komponenten verschoben oder gedreht werden. Ich denke, es ist “sinnvoller” (und “richtiger”), sich vorzustellen, dass man eine leere Fläche hat (egal, in was für einer Component die liegt), und dann das Bild in dieser Fläche verschieben und drehen will. Und dann … nur noch alle Operationen “rückwärts” aufschreiben :smiley: