AffineTransform bei Sprites

tuedel

Aktives Mitglied
Hallo an alle.

Es geht um folgendes. Bei einer Animation wird in meinem Programm on the fly eine Image einer Rotation, Translation und Skalierung unterzogen. Das sieht so aus:

Java:
public void drawSprite(Graphics2D g, double cog, double x, double y, double w, double h) {
       
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);                
        
        pos_x = x;
        pos_y = y;

        transform.setToIdentity();
        // Compute the corner, the drawing needs to start with
        transform.translate(x - (w / 2.0), y - (h / 2.0));
        transform.rotate(Math.toRadians(cog), w / 2.0, h / 2.0);
        transform.scale(w / spriteWidth, h / spriteHeight);        
               
        
        g.drawImage(sprite, transform, null);
    }

Meine Frage ergibt sich wie folgt - Ich habe wie im Qtext zu sehen verschiedene RenderingHints gesetzt um die Qualität der Rotation und Skalierung zu verbessern.
Allerdings habe ich den Eindruck, dass die Qualität dennoch überschaubar ist. Bei der Animation ist es nahezu möglich die Pixel der Kanten bei der Animation - der Veränderung pro Zeiteinheit - zu beobachten. Es handelt sich initial um ein hochauflösendes Bild von 528x528, dass auf ca. 20-30 Pixel runterskaliert wird.

Ich bin letztlich mit den Kanten nach der Rotation Weise unzufrieden und wollte mich erkundigen, ob es weitere Möglichkeiten gibt, mit Hilfe der Java2D Api die Transformationsqualität zu beeinflussen.

Herzlichen Dank.
Lg
 

Marco13

Top Contributor
Beschreib' mal genauer, was du meinst ... Welche Kanten meinst du? Kommen in dem Sprite irgendwelche Linen vor, die durch das Skalieren vielleicht "weniger als 1 pixel breit" werden?
 

tuedel

Aktives Mitglied
Hm sagen wir mal so, die Rundungen, die die Grenzen des Sprites ausmachen, werden rotiert und dadurch habe ich sehr scharfe pixelübergänge bei einer sehr kleinen skalierung.

Dadurch wird bei fortlaufender Animation und Drehung halt quasi auch mal eine Zeile dann "kürzer", wodurch die Kanten während der Animation etwas komisch aussehen. Eigentlich würde es wahrscheinich besser aussehen, wenn bei der Rotation die Kanten "schwammiger" aussehen würden, wodurch die Übergänge zwischen Bild und Hintergrund nicht so hart aussehen.

Ist halt so, dass bei wenigen Pixeln und kleinen Rotationen nicht alle Pixel bewegt werden müssen.

Ist irgendwie schwierig zu beschreiben..
 

tuedel

Aktives Mitglied
Sry das ich mich solange nicht gemeldet habe. Ich hatte mir ein paar Quellen durchgelesen und ein paar Sachen ausprobiert.

Letztlich bin ich auf die folgende Quelle gestoßen:

The Perils of Image.getScaledInstance() | Java.net

Im Resultat musste ich feststellen, dass die multi-step Variante nicht nur recht "verschwommene" skalierte Bilder erzeugt hat, sondern diese an den Rändern auch leicht verpixelt waren. Akzeptabel hat sich die scaledInstance Variante gezeigt, die jedoch auch zu sehr "schwammigen" bzw. unscharfen Resultaten führt.
Für diese Variante habe ich die Bilder zuerst skaliert und anschließend per AffineTransform nach Bedarf rotiert.

Letzte Variante ist die Skalierung mit der AfffineTransform gleichzeitig mit der Rotierung durchzuführen. Die Bilder sehen prinzipiell schärfer aus, haben aber auch leichte Verpixelungen, gerade bei rotierten Linien kann man das erkennen.

Ich bin mir nicht sicher, wieviel man mit der Java API rausholen kann, aber so richtig zufrieden bin ich mit der Qualität noch immer nicht. Folgend habe ich Beispielbilder angehangen.

Ich habe beiläufig ab und wann noch was von AffineTransformOp gelesen oder RescaleOp. Habe mich aber für deren Zweck, Handhabung und Ergebnisse noch nicht ausreichend belesen.
Erfahrungen von eurer Seite würden mich brennend interessieren.

Quelltext scaledInstance:

Java:
 for (int i = 0; i <= (JMain.getInstance().getMap().getMaxZoom() - JMain.getInstance().getMap().getMinZoom()); i++) {
            
            double targetWidth = newColorCache.getWidth() / 10 - (i * 6);
            double targetHeight = newColorCache.getHeight() / 10 - (i * 6);

            Image img = newColorCache.getScaledInstance((int) targetWidth, (int) targetHeight, BufferedImage.SCALE_SMOOTH);
            scaleCache.put(i, DefaultResources.drawBufferedImage(img));

        }

Quelltext AffineTransform:

Java:
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);

        transform.setToIdentity();
        // Compute the corner, the drawing needs to start with        
        transform.translate(x - (w / 2.0), y - (h / 2.0));
        transform.rotate(Math.toRadians(cog), w / 2.0, h / 2.0);        
        transform.scale(w / spriteWidth, h / spriteHeight);

        g.drawImage(sprite, transform, null);


Lg
tuedel
 

Anhänge

  • scaledinstance_right affine_left.png
    scaledinstance_right affine_left.png
    67,4 KB · Aufrufe: 36
  • scaled_zoom1.png
    scaled_zoom1.png
    6,1 KB · Aufrufe: 36
  • transform_zoom1.png
    transform_zoom1.png
    4,2 KB · Aufrufe: 36

Robokopp

Bekanntes Mitglied
Hallo,

ich hab das vor einiger Zeit mal für ein Projekt in der Uni gebraucht und wäre fast verzweifelt, weil sich immer irgendwas verzerrt hat^^
Dann bin ich auf folgende Lösung gestoßen:


Java:
	public void rotate(final Graphics g) {
		Graphics2D g2d = (Graphics2D) g;
		int w = (int) this.getWidth();
		int h = (int) this.getHeight();
		int x = (int) (this.getX() - w / 2);
		int y = (int) (this.getY() - h / 2);
		int xRot = x + w / 2;
		int yRot = y + h / 2;
		AffineTransform rotation = g2d.getTransform();
		rotation.rotate(Math.toRadians(this.getRotation() + 90), xRot, yRot);
		g2d.setTransform(rotation);
		g2d.drawImage(this.getImage(), x, y, w, h, null);
		g2d.rotate(-Math.toRadians(this.getRotation() + 90), (xRot), (yRot));

	}

Die Methode rufe ich einfach aus meiner drawObjects() Methode des drehbaren Objekts auf und übergebe ihr das Graphicsobjekt.


MfG
 

tuedel

Aktives Mitglied
Grüß Dich,

erstmal danke für die Antwort. Letztlich habe ich die Variante allerdings schon ausprobiert. Bei deiner wird die Skalierung durch drawImage vollzogen, statt durch die AffineTransform. Nach der Drehung Stelle ich dann vergleichbare Qualitäten fest.

Was mir gerade aufgefallen ist, ist ein anderer Effekt. Wenn ich über die AffineTransform die Position für den Draw festlege, so sind da double Values möglich. Die Animation ist also nicht auf Ganze Pixel festgesetzt. Dabei kommt irgendeine Art von Interpolation zum tragen, die darin resultiert, dass die Bilder bei der Bewegung "komisch" aussehen. Wenn ich das auf Ganze pixel runde durch cast bspw. ist das soweit recht hübsch, solang die Animation nicht allzu langsam abläuft. In Normaler Abspielrate machen die aber auch nur 1 pixel / max 3-4 Frames gut, wodurch die Animation etwas choppy aussieht.

Wie könnte man den draw beeinflussen um langsame Bewegungen durch double values für die Pixel nicht choppy aber qualitativ gut aussehen zu lassen ?!

Herzlichen Dank.
tuedel
 

Marco13

Top Contributor
@Robokopp: Das ist eine (etwas unkonventionell geschriebene) Variante dessen, was schon gepostet wurde.

@tuedel: Ja, die Perils sind der Standard-Link (hier im Forum wohl schon ca. 50 mal gepostet, link ;) )

Häng' vielleicht mal das Original-Sprite an (und was die gewünschte Skalierung ist), vielleicht kann man sich's dann ansehen. Beim ersten draufschauen sieht auch ein Bild geantialiast aus und das andere nicht...
 

tuedel

Aktives Mitglied
Hallo Marco,

im Anhang befindet sich das Original. Die Skalierung erfolgt auf Basis der zoom Stufe.

Java:
return sprite.getWidth(null) / 10 - (zoomLevel * 6);
return sprite.getHeight(null) / 10 - (zoomLevel * 6);
 

Anhänge

  • boat_normal.png
    boat_normal.png
    26,4 KB · Aufrufe: 33

Marco13

Top Contributor
"Zoom-Stufe" impliziert schon dass man nicht kontinuierlich zoomen kann - NUR dann macht das Erstellen der Scaled Instances auch Sinn.

Das hat mich jetzt aber auch mal interessiert. Deswegen habe ich mal so einen kleinen Test gebastelt (Screenshot im Anhang). Überraschenderweise scheint man richtig schöne Bilder nur zu bekommen, wenn man sich eine skalierte Instanz erstellt. Beim Skalieren "on the fly" sieht es immer pixelig aus, egal welche RenderingHints man setzt. Das hatte ich nicht erwartet. Ich dachte, dass er sich bei Antialiasing und Interpolation=Bicubic zumindest Mühe geben würde, auch die on-the-fly skalierten Bilder einigermaßen "glatt" zu malen. Aber das kann natürlich auch alles von Betriebssystem, Java-Version, Grafikkarte, Treiber etc. abhängen....

(BTW: There's no free lunch - du solltest berücksichtigen, dass "schönes" Zeichnen mit Bilinearem Glätten oder Anit-Aliasing (je nach Hardware-Aussattung und -Unterstützung) deutlich langsamer sein kann. Ich weiß ja nicht, wie viele dieser Boote du malen willst...)

Java:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
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.SwingUtilities;

public class ImageResizing
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }
    
    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final ImagePanel imagePanel = new ImagePanel();
        
        BufferedImage image = null;
        try
        {
            image = ImageIO.read(new File("boat_normal.png"));
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        int w = image.getWidth();
        int h = image.getHeight();
        int scaledW = w / 10;
        int scaledH = h / 10;
        Image scaledImage = image.getScaledInstance(scaledW, scaledH, Image.SCALE_SMOOTH);
        imagePanel.scaledImage = scaledImage;
        imagePanel.image = image;
        
        
        f.getContentPane().add(imagePanel);
        f.setSize(700,400);
        f.setVisible(true);
        
        Thread t = new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                while (true)
                {
                    imagePanel.angleDeg+=3;
                    imagePanel.repaint();
                    try
                    {
                        Thread.sleep(400);
                    }
                    catch (InterruptedException e)
                    {
                        Thread.currentThread().interrupt();
                        return;
                    }
                }
            }
        });
        t.setDaemon(true);
        t.start();
    }
    
}


class ImagePanel extends JPanel
{
    public Image image;
    public Image scaledImage;
    public double angleDeg;
    
    @Override
    protected void paintComponent(Graphics gr)
    {
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D)gr;
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, getWidth(), getHeight());
        g.setColor(Color.BLACK);
        
        AffineTransform oldAT = g.getTransform();
        int w = scaledImage.getWidth(this);
        int h = scaledImage.getHeight(this);
        double angleRad = Math.toRadians(angleDeg);
        
        g.drawString("Top: Using scaled instance", 10, 20);
        g.drawString("Center: Using original instance, scaling on the fly using Graphics2D#scale", 10, 40);
        g.drawString("Bottom: Using original instance, scaling on the fly using Graphics#drawImage(image,0,0,w,h,null)", 10, 60);
        
        g.translate(100, 100);
        g.drawString("Initial", 0, 0);
        g.drawImage(scaledImage, 0, 0, null);
        g.setTransform(oldAT);

        g.translate(100, 200);
        g.drawString("Initial", 0, 0);
        g.scale(0.1, 0.1);
        g.drawImage(image, 0, 0, null);
        g.setTransform(oldAT);

        g.translate(100, 300);
        g.drawString("Initial", 0, 0);
        g.drawImage(image, 0, 0, w, h, null);
        g.setTransform(oldAT);


        g.translate(200, 100);
        g.drawString("+Rotated", 0, 0);
        g.rotate(angleRad, w/2, h/2);
        g.drawImage(scaledImage, 0, 0, null);
        g.setTransform(oldAT);

        g.translate(200, 200);
        g.drawString("+Rotated", 0, 0);
        g.rotate(angleRad, w/2, h/2);
        g.scale(0.1, 0.1);
        g.drawImage(image, 0, 0, null);
        g.setTransform(oldAT);
        
        g.translate(200, 300);
        g.drawString("+Rotated", 0, 0);
        g.rotate(angleRad, w/2, h/2);
        g.drawImage(image, 0, 0, w, h, null);
        g.setTransform(oldAT);
        
        
        g.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING, 
            RenderingHints.VALUE_ANTIALIAS_ON);

        g.translate(300, 100);
        g.drawString("+Anti-aliased", 0, 0);
        g.rotate(angleRad, w/2, h/2);
        g.drawImage(scaledImage, 0, 0, null);
        g.setTransform(oldAT);
        
        g.translate(300, 200);
        g.drawString("+Anti-aliased", 0, 0);
        g.rotate(angleRad, w/2, h/2);
        g.scale(0.1, 0.1);
        g.drawImage(image, 0, 0, null);
        g.setTransform(oldAT);
        
        g.translate(300, 300);
        g.drawString("+Anti-aliased", 0, 0);
        g.rotate(angleRad, w/2, h/2);
        g.drawImage(image, 0, 0, w, h, null);
        g.setTransform(oldAT);
        

        g.setRenderingHint(
            RenderingHints.KEY_INTERPOLATION, 
            RenderingHints.VALUE_INTERPOLATION_BILINEAR);

        g.translate(400, 100);
        g.drawString("+Bilinear", 0, 0);
        g.rotate(angleRad, w/2, h/2);
        g.drawImage(scaledImage, 0, 0, null);
        g.setTransform(oldAT);
        
        g.translate(400, 200);
        g.drawString("+Bilinear", 0, 0);
        g.rotate(angleRad, w/2, h/2);
        g.scale(0.1, 0.1);
        g.drawImage(image, 0, 0, null);
        g.setTransform(oldAT);

        g.translate(400, 300);
        g.drawString("+Bilinear", 0, 0);
        g.rotate(angleRad, w/2, h/2);
        g.drawImage(image, 0, 0, w, h, null);
        g.setTransform(oldAT);

        
        
        g.setRenderingHint(
            RenderingHints.KEY_INTERPOLATION, 
            RenderingHints.VALUE_INTERPOLATION_BICUBIC);

        g.translate(500, 100);
        g.drawString("+Bicubic", 0, 0);
        g.rotate(angleRad, w/2, h/2);
        g.drawImage(scaledImage, 0, 0, null);
        g.setTransform(oldAT);
        
        g.translate(500, 200);
        g.drawString("+Bicubic", 0, 0);
        g.rotate(angleRad, w/2, h/2);
        g.scale(0.1, 0.1);
        g.drawImage(image, 0, 0, null);
        g.setTransform(oldAT);

        g.translate(500, 300);
        g.drawString("+Bicubic", 0, 0);
        g.rotate(angleRad, w/2, h/2);
        g.drawImage(image, 0, 0, w, h, null);
        g.setTransform(oldAT);

        

        
        
    }
}
 

Anhänge

  • ImageResizing01.png
    ImageResizing01.png
    22,8 KB · Aufrufe: 37

tuedel

Aktives Mitglied
Hallo Marco,

herzlichen Dank für dein Interesse und die Arbeit. Die Varianten bilinear scaled und bilinear on the fly hatte ich im ersten Post ja auch mal mit angehangen. Folglich muss man einfach iwo Abstriche machen. Die scaledInstance Variante sieht prinzipiell weicher aus, erscheint in der Anwendung dann aber trotzdem iwie ziemlich schwammig. Die Bicubic on the fly mit transform wirkt schärfer, aber einzelne Pixel bleiben schlichtweg erkennbar.

Um dem Problem entgegenzuwirken, zuviel Performance Last zu erzeugen, habe ich ein Caching auf Farbe, Zoom und Winkel gebastelt. Dennoch frisst die scaledInstance ziemlich viel Zeit und ist laut dem was ich gelesen habe deutlich langsamer. Da muss ich mich dann wohl mit dem on the fly transform anfreunden.

Herzlichen Dank.
Lg
tuedel
 

tuedel

Aktives Mitglied
Wobei ich mich frage, ob die Qualität rein aus der Skalierung so schlecht erscheint. Wenn das Bild initial nicht so groß wäre und von PS oder was vorher skaliert wird, dass Ganze besser ausschaut.
 

Marco13

Top Contributor
Folglich muss man einfach iwo Abstriche machen. Die scaledInstance Variante sieht prinzipiell weicher aus, erscheint in der Anwendung dann aber trotzdem iwie ziemlich schwammig. Die Bicubic on the fly mit transform wirkt schärfer, aber einzelne Pixel bleiben schlichtweg erkennbar.

Ehrlich gesagt sehe ich keinen Unterschied zwischen den "on-the-fly"-skalierten (egal, mit welchen Settings). Und die scaledInstance mit BILINEAR sieht IMHO gut aus, ich wüßte kaum, was man da besser machen sollte (irgendwo stößt die Auflösung eben an Grenzen, ein Bild wie
Code:
XXX
X X 
XXX
(aus schwarzen und weißen Pixeln) kann man eben kaum "vernünftig" um 30° gedreht darstellen). Ggf. kann man durch die im Perils-Link beschriebenen Verfahren noch ein bißchen was rausholen, aber "dramatisch viel besser" kann das IMHO kaum werden.



Um dem Problem entgegenzuwirken, zuviel Performance Last zu erzeugen, habe ich ein Caching auf Farbe, Zoom und Winkel gebastelt. Dennoch frisst die scaledInstance ziemlich viel Zeit und ist laut dem was ich gelesen habe deutlich langsamer. Da muss ich mich dann wohl mit dem on the fly transform anfreunden.

Die on-the-fly-Transform ist prinzipbedingt beim Zeichnen langsamer. Und ein 1000x1000 Pixel großes Bild bei jedem Zeichnen (!) auf 50x50 runterzurechnen wäre vermutlich (!) der Performance-Killer. Je nach Anwendungsfall könnte die eine oder andere Strategie eben sinnvoller sein:
- Häufiges, freies, kontinuierliches Zoomen in großem Bereich? Eher on-the-fly skalieren
- Schrittweises, seltene(re)s Zoomen auf wenigen Stufen? Eher gecachte, skalierte Instanzen
Zwischendinger könnte man sich noch überlegen - auch abhängig davon, wie viele (verschiedene) Bilder es gibt usw. Das getScaledInstance ist eben EINmal relativ teuer (und die Instanz belegt natürlich Speicher!), aber dafür braucht nicht mehr bei jedem Zeichnen skaliert zu werden.
 

tuedel

Aktives Mitglied
Deine Argumente sind alle logisch und ich hatte auch im Netz danach geschaut, welche Möglichkeiten es gibt um die Qualität bei der Rotation zu beeinflussen. Jetzt habe ich aber testweise einfach mal per GIMP auf 60x60 vorskaliert und die Qualität ist ungemein besser. Die Qualität allein der skalierten Instanz ist wie es aussieht der springende Punkt.

Btw. ich habe verschiedene Zoomstufen. Habe mich deswegen auch für das Caching entschieden.
 

Marco13

Top Contributor
Nun, die skalierte Instanz im Screenshot (ganz oben Links) sieht ja noch gut aus. Wenn man das dann ("Nur" mit AntiAliasing) gedreht zeichnet... *brech* ... aber mit BILINEAR ist's wieder schön. Es kann also nicht NUR an der skalierten Instanz selbst liegen (und dort könnte man, wie gesagt, noch einiges Drehen - welchen Filter hast du bei GIMP verwendet?)
 

Marco13

Top Contributor
Mit Lanczos hatte ich schon fast gerechnet ;) evtl. gibt es eine Implementierung davon, müßte ich jetzt aber auch erst Websuchen. Aber... sehen damit die gedrehten Bilder besser aus als die oben rechts im Screenshot? (Wie, das würde mich mal interessieren...?!)
 

tuedel

Aktives Mitglied
Ich hatte die Tage iwo was davon gelesen. Es gibt auf jeden Fall die Möglichkeit. Die Bilder sehen eindeutig besser aus als die Darstellung rechts oben. Gerade bei der Animation fällt das besonders auf. Das sieht einfach flüssiger aus.
 

Ähnliche Java Themen

Neue Themen


Oben