How to improve the performance of g.drawImage() method for resizing images

I have an application where users are able to upload pictures in albums but naturally the uploaded images need to be resized so there are also thumbs available and the shown pictures also fit in the page (eg. 800x600). The way I do the resize is like this:

Image scaledImage = img.getScaledInstance((int)width, (int)height, Image.SCALE_SMOOTH);
BufferedImage imageBuff = new BufferedImage((int)width, (int)height, BufferedImage.TYPE_INT_RGB);
Graphics g = imageBuff.createGraphics();
g.drawImage(scaledImage, 0, 0, new Color(0,0,0), null);
g.dispose();

And it works okayish. My only problem is that the g.drawImage() method seems to be awfully slow, and I just cannot imagine the user to be patient enought to wait for an upload of 20 pictures 20*10 secs ~ 3 minutes. In fact, on my computer it takes almost 40 secs for making the 3 different resizes for a single picture.

That's not good enough, and I'm looking for a faster solution. I'm wondering if somebody could tell me about a better one in Java OR by calling a shell script, command, whatever hack you know, it has to be quicker, everything else does not matter this time.


I'm using code similar to the following to scale images, I removed the part that deals with preserving the aspect ratio. The performance was definitely better than 10s per image, but I don't remember any exact numbers. To archive better quality when downscaling you should scale in several steps if the original image is more than twice the size of the wanted thumbnail, each step should scale the previous image to about half its size.

public static BufferedImage getScaledImage(BufferedImage image, int width, int height) throws IOException {
    int imageWidth  = image.getWidth();
    int imageHeight = image.getHeight();

    double scaleX = (double)width/imageWidth;
    double scaleY = (double)height/imageHeight;
    AffineTransform scaleTransform = AffineTransform.getScaleInstance(scaleX, scaleY);
    AffineTransformOp bilinearScaleOp = new AffineTransformOp(scaleTransform, AffineTransformOp.TYPE_BILINEAR);

    return bilinearScaleOp.filter(
        image,
        new BufferedImage(width, height, image.getType()));
}

Do you really need the quality that is provided by using Image.SCALE_SMOOTH? If you don't, you can try using Image.SCALE_FAST. You might find this article helpful if you want to stick with something provided by Java.


Well, Jacob and I wanted to resize an Image, not a BufferedImage. So we ended up with this code:

/**
 * we want the x and o to be resized when the JFrame is resized
 *
 * @param originalImage an x or an o. Use cross or oh fields.
 *
 * @param biggerWidth
 * @param biggerHeight
 */
private Image resizeToBig(Image originalImage, int biggerWidth, int biggerHeight) {
    int type = BufferedImage.TYPE_INT_ARGB;


    BufferedImage resizedImage = new BufferedImage(biggerWidth, biggerHeight, type);
    Graphics2D g = resizedImage.createGraphics();

    g.setComposite(AlphaComposite.Src);
    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

    g.drawImage(originalImage, 0, 0, biggerWidth, biggerHeight, this);
    g.dispose();


    return resizedImage;
}

The main point of the question was about the performance of scaling images in Java. The other answers, showed different approaches, without evaluating them further. I was curious about this as well, so I tried to write a small performance test. However, testing the image scaling performance reliably, sensibly and objectively is difficult. There are far too many infuencing factors to be taken into account:

  • The size of the input image
  • The size of the output image
  • The interpolation (i.e. "quality": Nearest neighbor, bilinear, bicubic)
  • The BufferedImage.TYPE_* of the input image
  • The BufferedImage.TYPE_* of the output image
  • The JVM version and operating system
  • Finally: The method that is actually used for performing the operation.

I tried to cover those that I considered the most important ones. The setup was:

  • The input is a simple, "average" photo (particularly, this "Image Of The Day" from Wikipedia, with a size of 2560x1706 pixels)

  • The main interpolation types are tested - namely, by using RenderingHints where the INTERPOLATION key was set to the values NEAREST_NEIGHBOR, BILINEAR and BICUBIC

  • The input image was converted to different types:

    • BufferedImage.TYPE_INT_RGB: A type that is commonly used, as it "usually" shows the best performance characteristics

    • BufferedImage.TYPE_3BTE_BGR: This is the type that it is read with by default, when just reading it with ImageIO

  • The target image size was varied between a width of 10000 (thus, scaling the image up), and 100 (thus, scaling the image down to thumbnail size)

The tests have been run on a Win64/AMD K10 with 3.7 GHz and JDK 1.8u31, with -Xmx4000m -server.

The tested methods are:

  • Using an AffineTransformOp, as in the answer by Jörn Horstmann
  • Using a Graphics, as in the answer by johnstosh
  • Using Image#getScaledInstance

The code of the tests is shown here:

import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.function.Supplier;

import javax.imageio.ImageIO;
import javax.swing.JLabel;

public class ImageScalingPerformance
{
    private static int blackHole = 0;

    public static void main(String[] args) throws IOException
    {
        // Image with size 2560 x 1706, from https://upload.wikimedia.org/
        //   wikipedia/commons/4/41/Pitta_moluccensis_-_Kaeng_Krachan.jpg
        BufferedImage image = ImageIO.read(
            new File("Pitta_moluccensis_-_Kaeng_Krachan.jpg"));

        int types[] =
        {
            BufferedImage.TYPE_3BYTE_BGR,
            BufferedImage.TYPE_INT_RGB,
        };
        Object interpolationValues[] =
        {
            RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR,
            RenderingHints.VALUE_INTERPOLATION_BILINEAR,
            RenderingHints.VALUE_INTERPOLATION_BICUBIC,
        };
        int widths[] =
        {
            10000, 5000, 2500, 1000, 500, 100
        };


        System.out.printf("%10s%22s%6s%18s%10s\n",
            "Image type", "Interpolation", "Size", "Method", "Duration (ms)");

        for (int type : types)
        {
            BufferedImage currentImage = convert(image, type);
            for (Object interpolationValue : interpolationValues)
            {
                for (int width : widths)
                {
                    List<Supplier<Image>> tests = 
                        createTests(currentImage, interpolationValue, width);

                    for (Supplier<Image> test : tests)
                    {
                        double durationMs = computeMs(test);

                        System.out.printf("%10s%22s%6s%18s%10s\n",
                            stringForBufferedImageType(type),
                            stringForInterpolationValue(interpolationValue),
                            String.valueOf(width), 
                            String.valueOf(test),
                            String.format(Locale.ENGLISH, "%6.3f", durationMs));
                    }
                }
            }
        }
        System.out.println(blackHole);
    }

    private static List<Supplier<Image>> createTests(
        BufferedImage image, Object interpolationValue, int width)
    {
        RenderingHints renderingHints = new RenderingHints(null);
        renderingHints.put(
            RenderingHints.KEY_INTERPOLATION, 
            interpolationValue);
        double scale = (double) width / image.getWidth();
        int height = (int)(scale * image.getHeight());

        Supplier<Image> s0 = new Supplier<Image>()
        {
            @Override
            public BufferedImage get()
            {
                return scaleWithAffineTransformOp(
                    image, width, height, renderingHints);
            }

            @Override
            public String toString()
            {
                return "AffineTransformOp";
            }
        };

        Supplier<Image> s1 = new Supplier<Image>()
        {
            @Override
            public Image get()
            {
                return scaleWithGraphics(
                    image, width, height, renderingHints);
            }

            @Override
            public String toString()
            {
                return "Graphics";
            }
        };

        Supplier<Image> s2 = new Supplier<Image>()
        {
            @Override
            public Image get()
            {
                return scaleWithGetScaledInstance(
                    image, width, height, renderingHints);
            }

            @Override
            public String toString()
            {
                return "GetScaledInstance";
            }
        };

        List<Supplier<Image>> tests = new ArrayList<Supplier<Image>>();
        tests.add(s0);
        tests.add(s1);
        tests.add(s2);
        return tests;
    }

    private static double computeMs(Supplier<Image> supplier)
    {
        int runs = 5;
        long before = System.nanoTime();
        for (int i=0; i<runs; i++)
        {
            Image image0 = supplier.get();
            blackHole += image0.hashCode();
        }
        long after = System.nanoTime();
        double durationMs = (after-before) / 1e6 / runs;
        return durationMs;
    }

    private static BufferedImage convert(BufferedImage image, int type)
    {
        BufferedImage newImage = new BufferedImage(
            image.getWidth(), image.getHeight(), type);
        Graphics2D g = newImage.createGraphics();
        g.drawImage(image, 0, 0, null);
        g.dispose();
        return newImage;
    }        

    private static BufferedImage scaleWithAffineTransformOp(
        BufferedImage image, int w, int h,
        RenderingHints renderingHints)
    {
        BufferedImage scaledImage = new BufferedImage(w, h, image.getType());
        double scaleX = (double) w / image.getWidth();
        double scaleY = (double) h / image.getHeight();
        AffineTransform affineTransform = 
            AffineTransform.getScaleInstance(scaleX, scaleY);
        AffineTransformOp affineTransformOp = new AffineTransformOp(
            affineTransform, renderingHints);
        return affineTransformOp.filter(
            image, scaledImage);
    }

    private static BufferedImage scaleWithGraphics(
        BufferedImage image, int w, int h,
        RenderingHints renderingHints) 
    {
        BufferedImage scaledImage = new BufferedImage(w, h, image.getType());
        Graphics2D g = scaledImage.createGraphics();
        g.setRenderingHints(renderingHints);
        g.drawImage(image, 0, 0, w, h, null);
        g.dispose();
        return scaledImage;
    }

    private static Image scaleWithGetScaledInstance(
        BufferedImage image, int w, int h,
        RenderingHints renderingHints)
    {
        int hint = Image.SCALE_REPLICATE;
        if (renderingHints.get(RenderingHints.KEY_ALPHA_INTERPOLATION) != 
            RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR)
        {
            hint = Image.SCALE_AREA_AVERAGING;
        }
        Image scaledImage = image.getScaledInstance(w, h, hint);
        MediaTracker mediaTracker = new MediaTracker(new JLabel());
        mediaTracker.addImage(scaledImage, 0);
        try
        {
            mediaTracker.waitForAll();
        }
        catch (InterruptedException e)
        {
            Thread.currentThread().interrupt();
        }
        return scaledImage;
    }

    private static String stringForBufferedImageType(int type)
    {
        switch (type)
        {
            case BufferedImage.TYPE_INT_RGB : return "INT_RGB";
            case BufferedImage.TYPE_INT_ARGB : return "INT_ARGB";
            case BufferedImage.TYPE_INT_ARGB_PRE : return "INT_ARGB_PRE";
            case BufferedImage.TYPE_INT_BGR : return "INT_BGR";
            case BufferedImage.TYPE_3BYTE_BGR : return "3BYTE_BGR";
            case BufferedImage.TYPE_4BYTE_ABGR : return "4BYTE_ABGR";
            case BufferedImage.TYPE_4BYTE_ABGR_PRE : return "4BYTE_ABGR_PRE";
            case BufferedImage.TYPE_USHORT_565_RGB : return "USHORT_565_RGB";
            case BufferedImage.TYPE_USHORT_555_RGB : return "USHORT_555_RGB";
            case BufferedImage.TYPE_BYTE_GRAY : return "BYTE_GRAY";
            case BufferedImage.TYPE_USHORT_GRAY : return "USHORT_GRAY";
            case BufferedImage.TYPE_BYTE_BINARY : return "BYTE_BINARY";
            case BufferedImage.TYPE_BYTE_INDEXED : return "BYTE_INDEXED";
        }
        return "CUSTOM";
    }

    private static String stringForInterpolationValue(Object value)
    {
        if (value == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR)
        {
            return "NEAREST/REPLICATE";
        }
        if (value == RenderingHints.VALUE_INTERPOLATION_BILINEAR)
        {
            return "BILINEAR/AREA_AVG";
        }
        if (value == RenderingHints.VALUE_INTERPOLATION_BICUBIC)
        {
            return "BICUBIC/AREA_AVG";
        }
        return "(unknown)";
    }


}

First, regarding getScaledInstance: As Chris Campbell has pointed out in his (famous) article about The Perils of Image.getScaledInstance() (which was already linked to in other answers), the Image#getScaledInstance method is somewhat broken, and has a distressingly bad performance for most configurations. Additionally, it has the disadvantage of not having such a fine-grained control about the interpolation type. This should be taken into account in the following performance comparison: The quality of the resulting images may differ, which is not considered here. For example, the "area averaging" method of getScaledInstance does not yield a good image quality when the image size is increased.

(The most severe drawback of Image#getScaledInstance is IMHO that it only delivers an Image, and not a BufferedImage, but if the image is only supposed to be painted into a Graphics, this may not be important)

I'll just dump the output of the program here for reference, some details will follow below:

Image type         Interpolation  Size            MethodDuration (ms)
 3BYTE_BGR     NEAREST/REPLICATE 10000 AffineTransformOp   197.287
 3BYTE_BGR     NEAREST/REPLICATE 10000          Graphics   184.427
 3BYTE_BGR     NEAREST/REPLICATE 10000 GetScaledInstance  1869.759
 3BYTE_BGR     NEAREST/REPLICATE  5000 AffineTransformOp    38.354
 3BYTE_BGR     NEAREST/REPLICATE  5000          Graphics    40.220
 3BYTE_BGR     NEAREST/REPLICATE  5000 GetScaledInstance  1088.448
 3BYTE_BGR     NEAREST/REPLICATE  2500 AffineTransformOp    10.153
 3BYTE_BGR     NEAREST/REPLICATE  2500          Graphics     9.461
 3BYTE_BGR     NEAREST/REPLICATE  2500 GetScaledInstance   613.030
 3BYTE_BGR     NEAREST/REPLICATE  1000 AffineTransformOp     2.137
 3BYTE_BGR     NEAREST/REPLICATE  1000          Graphics     1.956
 3BYTE_BGR     NEAREST/REPLICATE  1000 GetScaledInstance   464.989
 3BYTE_BGR     NEAREST/REPLICATE   500 AffineTransformOp     0.861
 3BYTE_BGR     NEAREST/REPLICATE   500          Graphics     0.750
 3BYTE_BGR     NEAREST/REPLICATE   500 GetScaledInstance   407.751
 3BYTE_BGR     NEAREST/REPLICATE   100 AffineTransformOp     0.206
 3BYTE_BGR     NEAREST/REPLICATE   100          Graphics     0.153
 3BYTE_BGR     NEAREST/REPLICATE   100 GetScaledInstance   385.863
 3BYTE_BGR     BILINEAR/AREA_AVG 10000 AffineTransformOp   830.097
 3BYTE_BGR     BILINEAR/AREA_AVG 10000          Graphics  1501.290
 3BYTE_BGR     BILINEAR/AREA_AVG 10000 GetScaledInstance  1627.934
 3BYTE_BGR     BILINEAR/AREA_AVG  5000 AffineTransformOp   207.816
 3BYTE_BGR     BILINEAR/AREA_AVG  5000          Graphics   376.789
 3BYTE_BGR     BILINEAR/AREA_AVG  5000 GetScaledInstance  1063.942
 3BYTE_BGR     BILINEAR/AREA_AVG  2500 AffineTransformOp    52.362
 3BYTE_BGR     BILINEAR/AREA_AVG  2500          Graphics    95.041
 3BYTE_BGR     BILINEAR/AREA_AVG  2500 GetScaledInstance   612.660
 3BYTE_BGR     BILINEAR/AREA_AVG  1000 AffineTransformOp     9.121
 3BYTE_BGR     BILINEAR/AREA_AVG  1000          Graphics    15.749
 3BYTE_BGR     BILINEAR/AREA_AVG  1000 GetScaledInstance   452.578
 3BYTE_BGR     BILINEAR/AREA_AVG   500 AffineTransformOp     2.593
 3BYTE_BGR     BILINEAR/AREA_AVG   500          Graphics     4.237
 3BYTE_BGR     BILINEAR/AREA_AVG   500 GetScaledInstance   407.661
 3BYTE_BGR     BILINEAR/AREA_AVG   100 AffineTransformOp     0.275
 3BYTE_BGR     BILINEAR/AREA_AVG   100          Graphics     0.297
 3BYTE_BGR     BILINEAR/AREA_AVG   100 GetScaledInstance   381.835
 3BYTE_BGR      BICUBIC/AREA_AVG 10000 AffineTransformOp  3015.943
 3BYTE_BGR      BICUBIC/AREA_AVG 10000          Graphics  5431.703
 3BYTE_BGR      BICUBIC/AREA_AVG 10000 GetScaledInstance  1654.424
 3BYTE_BGR      BICUBIC/AREA_AVG  5000 AffineTransformOp   756.136
 3BYTE_BGR      BICUBIC/AREA_AVG  5000          Graphics  1359.288
 3BYTE_BGR      BICUBIC/AREA_AVG  5000 GetScaledInstance  1063.467
 3BYTE_BGR      BICUBIC/AREA_AVG  2500 AffineTransformOp   189.953
 3BYTE_BGR      BICUBIC/AREA_AVG  2500          Graphics   341.039
 3BYTE_BGR      BICUBIC/AREA_AVG  2500 GetScaledInstance   615.807
 3BYTE_BGR      BICUBIC/AREA_AVG  1000 AffineTransformOp    31.351
 3BYTE_BGR      BICUBIC/AREA_AVG  1000          Graphics    55.914
 3BYTE_BGR      BICUBIC/AREA_AVG  1000 GetScaledInstance   451.808
 3BYTE_BGR      BICUBIC/AREA_AVG   500 AffineTransformOp     8.422
 3BYTE_BGR      BICUBIC/AREA_AVG   500          Graphics    15.028
 3BYTE_BGR      BICUBIC/AREA_AVG   500 GetScaledInstance   408.626
 3BYTE_BGR      BICUBIC/AREA_AVG   100 AffineTransformOp     0.703
 3BYTE_BGR      BICUBIC/AREA_AVG   100          Graphics     0.825
 3BYTE_BGR      BICUBIC/AREA_AVG   100 GetScaledInstance   382.610
   INT_RGB     NEAREST/REPLICATE 10000 AffineTransformOp   330.445
   INT_RGB     NEAREST/REPLICATE 10000          Graphics   114.656
   INT_RGB     NEAREST/REPLICATE 10000 GetScaledInstance  2784.542
   INT_RGB     NEAREST/REPLICATE  5000 AffineTransformOp    83.081
   INT_RGB     NEAREST/REPLICATE  5000          Graphics    29.148
   INT_RGB     NEAREST/REPLICATE  5000 GetScaledInstance  1117.136
   INT_RGB     NEAREST/REPLICATE  2500 AffineTransformOp    22.296
   INT_RGB     NEAREST/REPLICATE  2500          Graphics     7.735
   INT_RGB     NEAREST/REPLICATE  2500 GetScaledInstance   436.779
   INT_RGB     NEAREST/REPLICATE  1000 AffineTransformOp     3.859
   INT_RGB     NEAREST/REPLICATE  1000          Graphics     2.542
   INT_RGB     NEAREST/REPLICATE  1000 GetScaledInstance   205.863
   INT_RGB     NEAREST/REPLICATE   500 AffineTransformOp     1.413
   INT_RGB     NEAREST/REPLICATE   500          Graphics     0.963
   INT_RGB     NEAREST/REPLICATE   500 GetScaledInstance   156.537
   INT_RGB     NEAREST/REPLICATE   100 AffineTransformOp     0.160
   INT_RGB     NEAREST/REPLICATE   100          Graphics     0.074
   INT_RGB     NEAREST/REPLICATE   100 GetScaledInstance   126.159
   INT_RGB     BILINEAR/AREA_AVG 10000 AffineTransformOp  1019.438
   INT_RGB     BILINEAR/AREA_AVG 10000          Graphics  1230.621
   INT_RGB     BILINEAR/AREA_AVG 10000 GetScaledInstance  2721.918
   INT_RGB     BILINEAR/AREA_AVG  5000 AffineTransformOp   254.616
   INT_RGB     BILINEAR/AREA_AVG  5000          Graphics   308.374
   INT_RGB     BILINEAR/AREA_AVG  5000 GetScaledInstance  1269.898
   INT_RGB     BILINEAR/AREA_AVG  2500 AffineTransformOp    68.137
   INT_RGB     BILINEAR/AREA_AVG  2500          Graphics    80.163
   INT_RGB     BILINEAR/AREA_AVG  2500 GetScaledInstance   444.968
   INT_RGB     BILINEAR/AREA_AVG  1000 AffineTransformOp    13.093
   INT_RGB     BILINEAR/AREA_AVG  1000          Graphics    15.396
   INT_RGB     BILINEAR/AREA_AVG  1000 GetScaledInstance   211.929
   INT_RGB     BILINEAR/AREA_AVG   500 AffineTransformOp     3.238
   INT_RGB     BILINEAR/AREA_AVG   500          Graphics     3.689
   INT_RGB     BILINEAR/AREA_AVG   500 GetScaledInstance   159.688
   INT_RGB     BILINEAR/AREA_AVG   100 AffineTransformOp     0.329
   INT_RGB     BILINEAR/AREA_AVG   100          Graphics     0.277
   INT_RGB     BILINEAR/AREA_AVG   100 GetScaledInstance   127.905
   INT_RGB      BICUBIC/AREA_AVG 10000 AffineTransformOp  4211.287
   INT_RGB      BICUBIC/AREA_AVG 10000          Graphics  4712.587
   INT_RGB      BICUBIC/AREA_AVG 10000 GetScaledInstance  2830.749
   INT_RGB      BICUBIC/AREA_AVG  5000 AffineTransformOp  1069.088
   INT_RGB      BICUBIC/AREA_AVG  5000          Graphics  1182.285
   INT_RGB      BICUBIC/AREA_AVG  5000 GetScaledInstance  1155.663
   INT_RGB      BICUBIC/AREA_AVG  2500 AffineTransformOp   263.003
   INT_RGB      BICUBIC/AREA_AVG  2500          Graphics   297.663
   INT_RGB      BICUBIC/AREA_AVG  2500 GetScaledInstance   444.497
   INT_RGB      BICUBIC/AREA_AVG  1000 AffineTransformOp    42.841
   INT_RGB      BICUBIC/AREA_AVG  1000          Graphics    48.605
   INT_RGB      BICUBIC/AREA_AVG  1000 GetScaledInstance   209.261
   INT_RGB      BICUBIC/AREA_AVG   500 AffineTransformOp    11.004
   INT_RGB      BICUBIC/AREA_AVG   500          Graphics    12.407
   INT_RGB      BICUBIC/AREA_AVG   500 GetScaledInstance   156.794
   INT_RGB      BICUBIC/AREA_AVG   100 AffineTransformOp     0.817
   INT_RGB      BICUBIC/AREA_AVG   100          Graphics     0.790
   INT_RGB      BICUBIC/AREA_AVG   100 GetScaledInstance   128.700

It can be seen that for almost all cases, getScaledInstance performs poorly compared to the other approaches (and the few cases where it seems to perform better can be explained by the lower quality when scaling up).

The AffineTransformOp based approach seems to perform best on average, with the only notable exception being a NEAREST_NEIGHBOR scaling of TYPE_INT_RGB images, where the Graphics-based approach seems to be consistently faster.

The bottom line is: The method using AffineTransformOp, as in the answer by Jörn Horstmann, seems to be the one that offers the best performance for most application cases.