Quality of Image after resize very low -- Java
Asked Answered
P

4

29

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. https://i.sstatic.net/Nn6S0.jpg

Good resize in Paint shop

What it looks like being converted like this. https://i.sstatic.net/uXZMD.jpg

Bad resize

Penicillium answered 2/1, 2013 at 1:32 Comment(6)
In what way is the quality lower? Can you post example images?Tryst
Are you sure it's the resizing and not that you're saving it as a JPG and perhaps the default JPG compression is too high? Have you tried saving it as an uncompressed format first?Reticular
1) For better help sooner, post an SSCCE. 2) JPG is inherently lossy. The compression/quality can be set at time of creation, as seen in this answer. 3) The uploaded images appear identical to me. It also is not an image I would see as being well suited to resizing, given the pale contrast of the 'white good' appliance against a white BG.Audreaaudres
To me the images look very different, more just the smoothness of the thumb in the first one. I need it to be closer to that level of quality. Working on setting the compression quality now.Penicillium
Have you tried VALUE_INTERPOLATION_BICUBIC instead of VALUE_INTERPOLATION_BILINEAR? According to the documentation bilinear only considers a 4x4 sample from the input while it's less specific about bicubic. Proper resizing requires a larger sampling window as the ratio of output to input gets smaller.Ahmad
The Java resized image has severe aliasing. Claiming the sized versions look the same is some form of denial. I worked around this problem using an ImageMagick integration.Kassel
H
17

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.

Harm answered 2/1, 2013 at 4:40 Comment(4)
As can be seen at bugs.sun.com/bugdatabase/view_bug.do?bug_id=6500894, there is no lanczos support for these routines you are using. But, as you can find in code.google.com/p/java-image-scaling (code.google.com/p/java-image-scaling/source/browse/trunk/src/…), implementing Lanczos is particularly easy.Harm
I have just gotten the Scalr library working with ULTRA_QUALITY which has turned up a pretty decent result with still quite small image size. Will have to do a comparison between the two.Penicillium
Ah woops -- just replied about imgscalr, didn't see this comment. Hope it works well for you, either library is a great choice!Shauna
+1 and all those bad mentioned is all that Java has :( (natively atleast)Helicon
A
21

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.

Abulia answered 2/1, 2013 at 4:7 Comment(3)
Shall have a crack at stepping it down instead of going in one step.Penicillium
Have decided to go with either java-image-scaling or Scalr to quickly scale the image down. Thanks for the input though, definitely a good point.Penicillium
+1 very nice mad, I think this too would be a good read for OP The Perils of Image.getScaledInstance() as it does show solutions for scaling images very smallHelicon
H
17

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.

Harm answered 2/1, 2013 at 4:40 Comment(4)
As can be seen at bugs.sun.com/bugdatabase/view_bug.do?bug_id=6500894, there is no lanczos support for these routines you are using. But, as you can find in code.google.com/p/java-image-scaling (code.google.com/p/java-image-scaling/source/browse/trunk/src/…), implementing Lanczos is particularly easy.Harm
I have just gotten the Scalr library working with ULTRA_QUALITY which has turned up a pretty decent result with still quite small image size. Will have to do a comparison between the two.Penicillium
Ah woops -- just replied about imgscalr, didn't see this comment. Hope it works well for you, either library is a great choice!Shauna
+1 and all those bad mentioned is all that Java has :( (natively atleast)Helicon
A
14

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.

Athodyd answered 30/3, 2016 at 21:44 Comment(1)
thanks for providing this benchmark, really helpfullReincarnation
S
13

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.

Shauna answered 2/1, 2013 at 15:59 Comment(10)
Let us hope that research on resampling stops forever, so you don't have to create a SUPERMEGABLASTER_QUALITY constant. Seriously, why is it called like that ?Harm
@Riyad, is there also a function to plug it onto a white background? I am converting images to fit within a 60x60 but then I want to extend the canvas around it to 70x70, so picture is 60x60 and whitespace all around the image to 70x70.Penicillium
I found the pad method, however is their one to do padding on certain sides? As in 5px top and bottom and 10px on the right and left.Penicillium
@Penicillium There is not a pad method that accepts the 4 params, I just filed that as a feature request.Shauna
@Harm If you have a constructive recommendation please let me know.Shauna
@RiyadKalla name the constant based on the algorithm used. But now you will have to deal with backward compatibility, so for some time (years maybe) the new constant name will live along the deprecated ULTRA_QUALITY constant.Harm
@Harm What's funny is I was researching other algorithms last night (thinking about what to add for 5.0) and realized how my name-selection early on was problomatic for exactly this reason. I always opted for the library to hide details and be "dead simple" to use, exposing names like "bilinear" and "bicubic" and concepts like "kernels" is terrible for new users. I don't think I've necessarily made a mistake, but your point is well taken :)Shauna
@RiyadKalla if that is the case in fact, then I would suggest to create an alias between constants. Then, for example, the documentation would say that ULTRA_QUALITY is always the best resampling method available in the library (according to some criteria), and its implementation might change in future versions, but in this version it is an alias to METHOD_X. But that is just something I would prefer if I were using the library.Harm
@Harm Good call on the aliases -- agree that is the right approach moving forward. Thanks for the feedback.Shauna
Ahh, saved the day. Thank you for creating this awesome library!Albion

© 2022 - 2024 — McMap. All rights reserved.