Quality of Image after resize very low -- Java

In the script it is going from around the 300x300 mark down to 60x60. Need to improve the overall image quality as it is coming out very poorly at the moment.

public static Boolean resizeImage(String sourceImg, String destImg, Integer Width, Integer Height, Integer whiteSpaceAmount) 
{
    BufferedImage origImage;

    try 
    {
        origImage = ImageIO.read(new File(sourceImg));
        int type = origImage.getType() == 0? BufferedImage.TYPE_INT_ARGB : origImage.getType();
        int fHeight = Height;
        int fWidth = Width;
        int whiteSpace = Height + whiteSpaceAmount; //Formatting all to squares so don't need two whiteSpace calcs..
        double aspectRatio;

        //Work out the resized dimensions
        if (origImage.getHeight() > origImage.getWidth()) //If the pictures height is greater than the width then scale appropriately.
        {
            fHeight = Height; //Set the height to 60 as it is the biggest side.

            aspectRatio = (double)origImage.getWidth() / (double)origImage.getHeight(); //Get the aspect ratio of the picture.
            fWidth = (int)Math.round(Width * aspectRatio); //Sets the width as created via the aspect ratio.
        }
        else if (origImage.getHeight() < origImage.getWidth()) //If the pictures width is greater than the height scale appropriately.
        {
            fWidth = Width; //Set the height to 60 as it is the biggest side.

            aspectRatio = (double)origImage.getHeight() / (double)origImage.getWidth(); //Get the aspect ratio of the picture.
            fHeight = (int)Math.round(Height * aspectRatio); //Sets the height as created via the aspect ratio.
        }

        int extraHeight = whiteSpace - fHeight;
        int extraWidth = whiteSpace - fWidth;

        BufferedImage resizedImage = new BufferedImage(whiteSpace, whiteSpace, type);
        Graphics2D g = resizedImage.createGraphics();
        g.setColor(Color.white);
        g.fillRect(0, 0, whiteSpace, whiteSpace);

        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(origImage, extraWidth/2, extraHeight/2, fWidth, fHeight, null);
        g.dispose();

        ImageIO.write(resizedImage, "jpg", new File(destImg));
    } 
    catch (IOException ex) 
    {
        return false;
    }

    return true;
}

Really just need to know if their is something I can plug in that will bump up the quality or if I need to look at something else entirely.

EDIT: Picture comparison.

Source, just picked a random washing machine from google. http://www.essexappliances.co.uk/images/categories/washing-machine.jpg

Washing Machine

The same picture converted in Photoshop to what I need it to be. http://imgur.com/78B1p

Good resize in Paint shop

What it looks like being converted like this. http://imgur.com/8WlXD

Bad resize


Solution 1:

Scaling an image down over a large range is inherently dangerous (from the point of view of quality), especially using a single step.

The recommended method is to use a divide and conquer method. Basically, you scale the image down in steps of 50% until you reach your desired size.

So, I took the original image of 650x748 and scaled it down to fit within a 60x60 region (52x60).

enter image description here

Divide and conquer compared to one step...

enter image description hereenter image description here

public class TestImageResize {

    public static void main(String[] args) {
        new TestImageResize();
    }

    public TestImageResize() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (Exception ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new ScalePane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class ScalePane extends JPanel {

        private BufferedImage original;
        private BufferedImage scaled;

        public ScalePane() {
            try {
                original = ImageIO.read(new File("path/to/master.jpg"));
                scaled = getScaledInstanceToFit(original, new Dimension(60, 60));
                ImageIO.write(scaled, "jpg", new File("scaled.jpg"));

                BufferedImage image = new BufferedImage(52, 60, BufferedImage.TYPE_INT_RGB);
                Graphics2D g2d = image.createGraphics();
                g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
                g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                g2d.drawImage(original, 0, 0, 52, 60, this);
                g2d.dispose();

                ImageIO.write(image, "jpg", new File("test.jpg"));

            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }

        @Override
        public Dimension getPreferredSize() {

            Dimension size = super.getPreferredSize();
            if (original != null) {
                if (scaled != null) {
                    size.width = original.getWidth() + scaled.getWidth();
                    size.height = original.getHeight();
                } else {
                    size.width = original.getWidth();
                    size.height = original.getHeight();
                }
            }

            return size;
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

            if (original != null) {
                int x = 0;
                int y = (getHeight() - original.getHeight()) / 2;;
                if (scaled != null) {
                    x = (getWidth() - (original.getWidth() + scaled.getWidth())) / 2;
                } else {
                    x = (getWidth() - original.getWidth()) / 2;
                }
                g2d.drawImage(original, x, y, this);

                if (scaled != null) {
                    x += original.getWidth();
                    y = (getHeight() - scaled.getHeight()) / 2;
                    g2d.drawImage(scaled, x, y, this);
                }
            }
            g2d.dispose();
        }

        public BufferedImage getScaledInstanceToFit(BufferedImage img, Dimension size) {
            float scaleFactor = getScaleFactorToFit(img, size);
            return getScaledInstance(img, scaleFactor);
        }

        public float getScaleFactorToFit(BufferedImage img, Dimension size) {
            float scale = 1f;
            if (img != null) {
                int imageWidth = img.getWidth();
                int imageHeight = img.getHeight();
                scale = getScaleFactorToFit(new Dimension(imageWidth, imageHeight), size);
            }
            return scale;
        }

        public float getScaleFactorToFit(Dimension original, Dimension toFit) {
            float scale = 1f;
            if (original != null && toFit != null) {
                float dScaleWidth = getScaleFactor(original.width, toFit.width);
                float dScaleHeight = getScaleFactor(original.height, toFit.height);
                scale = Math.min(dScaleHeight, dScaleWidth);
            }
            return scale;
        }

        public float getScaleFactor(int iMasterSize, int iTargetSize) {
            float scale = 1;
            if (iMasterSize > iTargetSize) {
                scale = (float) iTargetSize / (float) iMasterSize;
            } else {
                scale = (float) iTargetSize / (float) iMasterSize;
            }
            return scale;
        }

        public BufferedImage getScaledInstance(BufferedImage img, double dScaleFactor) {
            BufferedImage imgBuffer = null;
            imgBuffer = getScaledInstance(img, dScaleFactor, RenderingHints.VALUE_INTERPOLATION_BILINEAR, true);
            return imgBuffer;
        }

        protected BufferedImage getScaledInstance(BufferedImage img, double dScaleFactor, Object hint, boolean higherQuality) {

            int targetWidth = (int) Math.round(img.getWidth() * dScaleFactor);
            int targetHeight = (int) Math.round(img.getHeight() * dScaleFactor);

            int type = (img.getTransparency() == Transparency.OPAQUE)
                            ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;

            BufferedImage ret = (BufferedImage) img;

            if (targetHeight > 0 || targetWidth > 0) {
                int w, h;
                if (higherQuality) {
                    w = img.getWidth();
                    h = img.getHeight();
                } else {
                    w = targetWidth;
                    h = targetHeight;
                }

                do {
                    if (higherQuality && w > targetWidth) {
                        w /= 2;
                        if (w < targetWidth) {
                            w = targetWidth;
                        }
                    }

                    if (higherQuality && h > targetHeight) {
                        h /= 2;
                        if (h < targetHeight) {
                            h = targetHeight;
                        }
                    }

                    BufferedImage tmp = new BufferedImage(Math.max(w, 1), Math.max(h, 1), type);
                    Graphics2D g2 = tmp.createGraphics();
                    g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
                    g2.drawImage(ret, 0, 0, w, h, null);
                    g2.dispose();

                    ret = tmp;
                } while (w != targetWidth || h != targetHeight);
            } else {
                ret = new BufferedImage(1, 1, type);
            }
            return ret;
        }
    }
}

You may, also, find The Perils of Image.getScaledInstance() of interest.

Solution 2:

The issue you are seeing is actually related to the resampling filter used for downscaling. Obviously, the one used by your library is a bad one for the situation. Nearest neighbor, bilinear and bicubic are typical bad examples to be used when downscaling. I don't know the exact resampling filter Photoshop uses, but I used 3-lobed lanczos and got the following result:

enter image description here

So, to solve your problem, you need to use a smarter resampling filter.

Solution 3:

dutchman, this is why I maintain the imgscalr library -- to make this kind of stuff painfully easy.

In your example, a single method call would do the trick, right after your first ImageIO.read line:

origImage = ImageIO.read(new File(sourceImg));

you can do the following to get what you want (javadoc for this method):

origImage = Scalr.resize(origImage, Method.ULTRA_QUALITY, 60);

and if that still looked a little jagged (because you are removing so much information from the image, you can add the following OP to the command to apply a light anti-aliasing filter to the image so it looks smoother):

origImage = Scalr.resize(origImage, Method.ULTRA_QUALITY, 60, Scalr.OP_ANTIALIAS);

That will replace all the remainder of the code logic you have. The only other thing I would recommend is saving out your really small samples as PNG's so there is no more compression/lossy conversion done on the image OR make sure you use little to none compression on the JPG if you really want it in JPG format. (Here is an article on how to do it; it utilizes the ImageWriteParam class)

imgscalr is licensed under an Apache 2 license and hosted on GitHub so you can do what you want with it; it also includes asynchronous scaling support if you are using the library in a server-side app and queuing up huge numbers of scaling operations and don't want to kill the server.

Solution 4:

As already stated, Java's Graphics2D does not provide a very good algorithm for down-scaling. If you don't want to implement a sophisticated algorithm yourself you could try out the current open source libs specialized for this: Thumbnailator, imgscalr and a Java interface for ImageMagick.

While researching for a private project I tried them out (except ImageMagick) and here are the visual results with Photoshop as reference:

comparison

A. Thumbnailator 0.4.8 with default settings (no additional internal resizing)
B. imgscalr 4.2 with ULTRA_QUALTY setting
C. Photoshop CS5 bicubic filter (save for web)
D. Graphics2d with all HQ render hints

Here is the used code

Thumbnailator and PS create similar results, while imgscalr seems to be softer. It is subjective which one of the libs creates the preferable results. Another point to consider though is the performance. While Thumbnailator and Graphics2d have similar runtime, imgscalr is considerably slower (with ULTRA_QUALITY) in my benchmarks.

For more info, read this post providing more detail on this matter.