Bilder in Java schnell skalieren / Fast Image Scaling / Resizing in Java

I’m recognizing, that this article faced quite some hits – if you’re a non-german user and want this article to be translated, please leave me a comment – maybe I’m gonna translate it if that is what people want.
Bilder in Java skalieren ist ein Thema für sich…

Intro

Verführerisch ist sie ja, die Image.getScaledImage()-Methode. Und ebenso fatal, denn sie ist eine heißer Kandidat den Code elends langsam zu machen. Wie geht’s schneller/besser? Ein wertvoller Link zum Einstieg ist The Perils of Image.getScaledInstance() und die dortigen Links.

Die obige Aussage (bzgl. langsam) ist ohne Zahlen quasi wertfrei. Gerade eben habe ich wieder ein Stück Code vor mir, indem Bilder skaliert werden müssen – und das schnell, da der Benutzer wartet! Ich habe also über den Daumen nicht mehr als 100-200ms Zeit, dem Benutzer  ein Ergebnis zu präsentieren, bevor die Anwendung langsam wirkt. Also:  meinen eigenen ImageScaler verwenden, den ich vor Zeiten mal geschrieben habe oder auf JAI (Java Advanced Imaging) zurückgreifen?
Der Plan ist klar: eine Performance-Messung muss her (da ich noch weiß, dass ich damals nicht mit JAI verglichen habe).

Messung / Ergebnisse

Input: Ein Jpeg 4008 x 2443 Pixel / 2,47 MB
Output: Das Bild soll in max. 400 x 400 Pixel eingebettet werden, Seitenverhältnis soll beibehalten werden (also ~400 x 243 Pixel).
Kandidaten:

  1. Image.getScaledInstance()
  2. mein eigener Scaler
  3. JAI

Setup: Bild ausserhalb der Zeitmessung einlesen, jeweils 20x skalieren und die benötigte Zeit ausgeben. JAI wird dabei mit nativen DLLs getestet ().

Ergebnis 1:

  1. Image.getScaledInstance(): ~30 ms
  2. mein eigener Scaler: ~234 ms
  3. JAI: ~500 ms

Moment – das ist NICHT was erwartet war.
Image img = src.getScaledInstance(400, -1, Image.SCALE_FAST);
Sieht aus als könnte man nicht viel falsch machen. – Aber wird da überhaupt etwas gemacht?
Verändern wir den Aufruf:
img.getSource().startProduction(new ImageConsumer() { … leere Methodenrümpfe… }

Ergebnis 2:

  1. Image.getScaledInstance(): 35781 ms
  2. mein Scaler:  ~234 ms
  3. JAI: ~500 ms

Aha. So sieht das aus wie erwartet. getScaledInstance() skaliert das Bild also erst bei Bedarf – leider sehr langsam.

Ergebnis 3 – 1x, 5x und 50x kleinskalieren:

  1. mein Scaler:  ~ 60 ms, ~ 90 ms, ~460 ms
  2. JAI: ~500 ms, 500 ms, 500 ms

Ahja – JAI cached offenbar. Interessant zu wissen – bei entsprechendem Szenario also sicher die bessere Wahl – diesmal gewinnt aber mein Scaler, da hier nur kleinskaliert werden soll und die restlichen JAI-Features eh ungenutzt bleiben.

Fazit

Das Key Feature meines Skalers ist die Essenz aus vielen Blogs und JavaOne-Folien:

  1. Solange das Bild größer als die doppelte Zielgröße ist: Bild mit Faktor 0.5 und Nearest Neighbor Interpolation skalieren. – Um nicht duzende Zwischenbilder erzeugen zu müssen (was bei großen Eingangsbildern richtig viel Speicher kosten kann, da die Bilder ja im Speicher dekomprimiert werden müssen!), skaliere ich in einem Schritt auf das kleinste Bild, das noch größer als das Zielbild ist.
  2. letzten Skalierungsschritt mit Bilinearer Interpolation skalieren.

Code:

public class ImageScaler {

    public BufferedImage scaleImage(BufferedImage img, Dimension d) {
        img = scaleByHalf(img, d);
        img = scaleExact(img, d);
        return img;
    }

    private BufferedImage scaleByHalf(BufferedImage img, Dimension d) {
        int w = img.getWidth();
        int h = img.getHeight();
        float factor = getBinFactor(w, h, d);

        // make new size
        w *= factor;
        h *= factor;
        BufferedImage scaled = new BufferedImage(w, h,
                BufferedImage.TYPE_INT_RGB);
        Graphics2D g = scaled.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
        g.drawImage(img, 0, 0, w, h, null);
        g.dispose();
        return scaled;
    }

    private BufferedImage scaleExact(BufferedImage img, Dimension d) {
        float factor = getFactor(img.getWidth(), img.getHeight(), d);

        // create the image
        int w = (int) (img.getWidth() * factor);
        int h = (int) (img.getHeight() * factor);
        BufferedImage scaled = new BufferedImage(w, h,
                BufferedImage.TYPE_INT_RGB);

        Graphics2D g = scaled.createGraphics();
        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        g.drawImage(img, 0, 0, w, h, null);
        g.dispose();
        return scaled;
    }

    float getBinFactor(int width, int height, Dimension dim) {
        float factor = 1;
        float target = getFactor(width, height, dim);
        if (target <= 1) { while (factor / 2 > target) { factor /= 2; }
        } else { while (factor * 2 < target) { factor *= 2; }         }
        return factor;
    }

    float getFactor(int width, int height, Dimension dim) {
        float sx = dim.width / (float) width;
        float sy = dim.height / (float) height;
        return Math.min(sx, sy);
    }
}

Nützliche Links:
public class ImageScaler {public BufferedImage scaleImage(BufferedImage img, Dimension d) {
img = scaleByHalf(img, d);
img = scaleExact(img, d);
return img;
}

private BufferedImage scaleByHalf(BufferedImage img, Dimension d) {
int w = img.getWidth();
int h = img.getHeight();
float factor = getBinFactor(w, h, d);

// make new size
w *= factor;
h *= factor;
BufferedImage scaled = new BufferedImage(w, h,
BufferedImage.TYPE_INT_RGB);
Graphics2D g = scaled.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
g.drawImage(img, 0, 0, w, h, null);
g.dispose();
return scaled;
}

private BufferedImage scaleExact(BufferedImage img, Dimension d) {
float factor = getFactor(img.getWidth(), img.getHeight(), d);

// create the image
int w = (int) (img.getWidth() * factor);
int h = (int) (img.getHeight() * factor);
BufferedImage scaled = new BufferedImage(w, h,
BufferedImage.TYPE_INT_RGB);

Graphics2D g = scaled.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(img, 0, 0, w, h, null);
g.dispose();
return scaled;
}

float getBinFactor(int width, int height, Dimension dim) {
float factor = 1;
float target = getFactor(width, height, dim);
if (target <= 1) { while (factor / 2 > target) { factor /= 2; }
} else { while (factor * 2 < target) { factor *= 2; }         }
return factor;
}

float getFactor(int width, int height, Dimension dim) {
float sx = dim.width / (float) width;
float sy = dim.height / (float) height;
return Math.min(sx, sy);
}
}