Java - resize image without losing quality
Asked Answered
R

7

71

I have 10,000 photos that need to be resized so I have a Java program to do that. Unfortunately, the quality of the image is poorly lost and I don't have access to the uncompressed images.

import java.awt.Graphics;
import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;


import javax.imageio.ImageIO;
/**
 * This class will resize all the images in a given folder
 * @author 
 *
 */
public class JavaImageResizer {

    public static void main(String[] args) throws IOException {

        File folder = new File("/Users/me/Desktop/images/");
        File[] listOfFiles = folder.listFiles();
        System.out.println("Total No of Files:"+listOfFiles.length);
        BufferedImage img = null;
        BufferedImage tempPNG = null;
        BufferedImage tempJPG = null;
        File newFilePNG = null;
        File newFileJPG = null;
        for (int i = 0; i < listOfFiles.length; i++) {
              if (listOfFiles[i].isFile()) {
                System.out.println("File " + listOfFiles[i].getName());
                img = ImageIO.read(new File("/Users/me/Desktop/images/"+listOfFiles[i].getName()));
                tempJPG = resizeImage(img, img.getWidth(), img.getHeight());
                newFileJPG = new File("/Users/me/Desktop/images/"+listOfFiles[i].getName()+"_New");
                ImageIO.write(tempJPG, "jpg", newFileJPG);
              }
        }
        System.out.println("DONE");
    }

    /**
     * This function resize the image file and returns the BufferedImage object that can be saved to file system.
     */
        public static BufferedImage resizeImage(final Image image, int width, int height) {
    int targetw = 0;
    int targeth = 75;

    if (width > height)targetw = 112;
    else targetw = 50;

    do {
        if (width > targetw) {
            width /= 2;
            if (width < targetw) width = targetw;
        }

        if (height > targeth) {
            height /= 2;
            if (height < targeth) height = targeth;
        }
    } while (width != targetw || height != targeth);

    final BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
    final Graphics2D graphics2D = bufferedImage.createGraphics();
    graphics2D.setComposite(AlphaComposite.Src);
    graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
    graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
    graphics2D.drawImage(image, 0, 0, width, height, null);
    graphics2D.dispose();

    return bufferedImage;
}

An image I am working with is this: Firwork - original - large

This is the manual resizing I've done in Microsoft Paint:

resize - using Paint - small

and this is the output from my program [bilinear]:

resize - using java program - small

UPDATE: No significant difference using BICUBIC

and this is the output from my program [bicubic]:

enter image description here

is there anyway to increase the quality of the program output so I don't have to manually resize all photos?

Thank you in advance!

Roaring answered 14/7, 2014 at 20:15 Comment(10)
possible duplicate of Re-sizing an image without loosing qualityAgraphia
@JordanD I've already looked at that and it did not resolve the issue....Roaring
you may have already tried this, but if not, have you tried doing bicubic interpolation instead of bilinear?Rectilinear
@Rectilinear I have tried that and saw no significant difference in the images. Thank you for the suggestion though!Roaring
possible duplicate of How to get a good quality thumbnailZounds
This is what linear interpolation looks like. You need to use Bi-Cubic to get proper results.Bellis
See this answer for workaround: https://mcmap.net/q/276314/-java-2d-image-resize-ignoring-bicubic-bilinear-interpolation-rendering-hints-os-x-linuxBellis
@Bellis Update to show how BICUBIC isn't any different from the BILINEAR. That answer and all other ones have been read and looked through. I have found no solution that I've worked through that satisfies so far... This is why I decided to ask my own question. Thank you for the suggested [ready looked through] reading :)Roaring
Possible duplicate of #11967958Slothful
Another similar question: How to resize the buffered image n graphics 2d in java?Motorman
D
99

Unfortunately, there is no recommended out-of-the-box scaling in Java that provides visually good results. Among others, here are the methods I recommend for scaling:

  • Lanczos3 Resampling (usually visually better, but slower)
  • Progressive Down Scaling (usually visually fine, can be quite fast)
  • One-Step scaling for up scaling (with Graphics2d bicubic fast and good results, usually not as good as Lanczos3)

Examples for every method can be found in this answer.

Visual Comparison

Here is your image scaled to 96x140 with different methods/libs. Click on the image to get the full size:

comparison

comparison zoom

  1. Morten Nobel's lib Lanczos3
  2. Thumbnailator Bilinear Progressive Scaling
  3. Imgscalr ULTRA_QUALTY (1/7 step Bicubic Progressive Scaling)
  4. Imgscalr QUALTY (1/2 step Bicubic Progressive Scaling)
  5. Morten Nobel's lib Bilinear Progressive Scaling
  6. Graphics2d Bicubic interpolation
  7. Graphics2d Nearest Neighbor interpolation
  8. Photoshop CS5 bicubic as reference

Unfortunately a single image is not enough to judge a scaling algorithm, you should test icons with sharp edges, photos with text, etc.

Lanczos Resampling

Is said to be good for up- and especially downscaling. Unfortunately there is no native implementation in current JDK so you either implement it yourself and use a lib like Morten Nobel's lib. A simple example using said lib:

ResampleOp resizeOp = new ResampleOp(dWidth, dHeight);
resizeOp.setFilter(ResampleFilters.getLanczos3Filter());
BufferedImage scaledImage = resizeOp.filter(imageToScale, null);

The lib is published on maven-central which is not mentioned, unfortunately. The downside is that it usually is very slow without any highly optimized or hardware accelerated implementations known to me. Nobel's implementation is about 8 times slower than a 1/2 step progressive scaling algorithm with Graphics2d. Read more about this lib on his blog.

Progressive Scaling

Mentioned in Chris Campbell's blog about scaling in Java, progressive scaling is basically incrementally scaling an image in smaller steps until the final dimensions are reached. Campbell describes it as halving width/height until you reach target. This produces good results and can be used with Graphics2D which can be hardware accelerated, therefore usually having very good performance with acceptable results in most cases. The major downside of this is if downscaled less than half using Graphics2D provides the same mediocre results since it is only scaled once.

Here is a simple example on how it works:

progressive scaling

The following libs incorporate forms of progressive scaling based on Graphics2d:

Thumbnailator v0.4.8

Uses the progressive bilinear algorithm if the target is at least half of every dimension, otherwise it uses simple Graphics2d bilinear scaling and bicubic for upscaling.

Resizer resizer = DefaultResizerFactory.getInstance().getResizer(
  new Dimension(imageToScale.getWidth(), imageToScale.getHeight()), 
  new Dimension(dWidth, dHeight))
BufferedImage scaledImage = new FixedSizeThumbnailMaker(
  dWidth, dHeight, false, true).resizer(resizer).make(imageToScale);

It is as fast or slightly faster than one-step scaling with Graphics2d scoring an average of 6.9 sec in my benchmark.

Imgscalr v4.2

Uses progressive bicubic scaling. In the QUALITY setting it uses Campbell style algorithm with halving the dimensions every step while the ULTRA_QUALITY has finer steps, reducing the size every increment by 1/7 which generates generally softer images but minimizes the instances where only 1 iteration is used.

BufferedImage scaledImage = Scalr.resize(imageToScale, Scalr.Method.ULTRA_QUALITY, Scalr.Mode.FIT_EXACT, dWidth, dHeight, bufferedImageOpArray);

The major downside is performance. ULTRA_QUALITY is considerably slower than the other libs. Even QUALITY a bit slower than Thumbnailator's implementation. My simple benchmark resulted in 26.2 sec and 11.1 sec average respectively.

Morten Nobel's lib v0.8.6

Has also implementations for progressive scaling for all basic Graphics2d (bilinear, bicubic & nearest neighbor)

BufferedImage scaledImage = new MultiStepRescaleOp(dWidth, dHeight, RenderingHints.VALUE_INTERPOLATION_BILINEAR).filter(imageToScale, null);

A word on JDK Scaling Methods

Current jdk way to scale an image would be something like this

scaledImage = new BufferedImage(dWidth, dHeight, imageType);
Graphics2D graphics2D = scaledImage.createGraphics();
graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics2D.drawImage(imageToScale, 0, 0, dWidth, dHeight, null);
graphics2D.dispose();

but most are very disappointed with the result of downscaling no matter what interpolation or other RenderHints are used. On the other hand upscaling seems to produce acceptable images (best would be bicubic). In previous JDK version (we're talking 90s v1.1) Image.getScaledInstance() was introduced which provided good visual results with parameter SCALE_AREA_AVERAGING but you are discouraged to use it - read the full explanation here.

Dariodariole answered 2/4, 2016 at 0:0 Comment(0)
I
47

Thumbnailator is a library that was written to create high-quality thumbnails in a simple manner, and doing a batch conversion of existing images is one of its use cases.

Performing batch resizing

For example, to adapt your example using Thumbnailator, you should be able to achieve similar results with the following code:

File folder = new File("/Users/me/Desktop/images/");
Thumbnails.of(folder.listFiles())
    .size(112, 75)
    .outputFormat("jpg")
    .toFiles(Rename.PREFIX_DOT_THUMBNAIL);

This will go ahead and takes all files in your images directory and proceed to process them one by one, try to resize them to fit in the dimensions of 112 x 75, and it will attempt to preserve the aspect ratio of the original image to prevent "warping" of the image.

Thumbnailator will go ahead and read all files, regardless of image types (as long as the Java Image IO supports the format, Thumbnailator will process it), perform the resizing operation and output the thumbnails as JPEG files, while tacking on a thumbnail. to the beginning of the file name.

The following is an illustration of how the file name of the original will be used in the file name of the thumbnail if the above code is executed.

images/fireworks.jpg     ->  images/thumbnail.fireworks.jpg
images/illustration.png  ->  images/thumbnail.illustration.png
images/mountains.jpg     ->  images/thumbnail.mountains.jpg

Generating high-quality thumbnails

In terms of image quality, as mentioned in Marco13's answer, the technique described by Chris Campbell in his The Perils of Image.getScaledInstance() is implemented in Thumbnailator, resulting in high-quality thumbnails without requiring any complicated processing.

The following is the thumbnail generated when resizing the fireworks image shown in the original question using Thumbnailator:

Thumbnail of image in original question

The above image was created with the following code:

BufferedImage thumbnail = 
    Thumbnails.of(new URL("https://i.sstatic.net/X0aPT.jpg"))
        .height(75)
        .asBufferedImage();

ImageIO.write(thumbnail, "png", new File("24745147.png"));

The code shows that it can also accept URLs as input, and that Thumbnailator is also capable of creating BufferedImages as well.


Disclaimer: I am the maintainer of the Thumbnailator library.

Intercurrent answered 22/2, 2015 at 14:8 Comment(2)
Does it support gif and animated webp yet?Nipissing
@Nipissing Thumbnailator relies on the Image I/O API for reading/writing images, so as long as a Image I/O plugin to support the format is loaded, Thumbnailator can support that format. A GIF reader/writer is included in the JRE, but for WebP, a 3rd-party plugin like TwelveMonkeys ImageIO would be necessary.Intercurrent
K
18

Given your input image, the method from the answer in the first link in the comments (kudos to Chris Campbell) produces one of the following thumbnails:

enter image description here enter image description here

(The other one is the thumbnail that you created with MS Paint. It's hard to call one of them "better" than the other...)

EDIT: Just to point this out as well: The main problem with your original code was that you did not really scale the image in multiple steps. You just used a strange loop to "compute" the target size. The key point is that you actually perform the scaling in multiple steps.

Just for completeness, the MVCE

(Edit: I mentioned Chris Campbell and referred to the source via the comments, but to make this more clear here: The following is based on the article The Perils of Image.getScaledInstance() )

import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;

public class ResizeQuality
{
    public static void main(String[] args) throws IOException
    {
        BufferedImage image = ImageIO.read(new File("X0aPT.jpg"));
        BufferedImage scaled = getScaledInstance(
            image, 51, 75, RenderingHints.VALUE_INTERPOLATION_BILINEAR, true);
        writeJPG(scaled, new FileOutputStream("X0aPT_tn.jpg"), 0.85f);
    }

    public static BufferedImage getScaledInstance(
        BufferedImage img, int targetWidth,
        int targetHeight, Object hint, 
        boolean higherQuality)
    {
        int type =
            (img.getTransparency() == Transparency.OPAQUE)
            ? BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
        BufferedImage ret = (BufferedImage) img;
        int w, h;
        if (higherQuality)
        {
            // Use multi-step technique: start with original size, then
            // scale down in multiple passes with drawImage()
            // until the target size is reached
            w = img.getWidth();
            h = img.getHeight();
        }
        else
        {
            // Use one-step technique: scale directly from original
            // size to target size with a single drawImage() call
            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(w, h, 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);

        return ret;
    }

    public static void writeJPG(
        BufferedImage bufferedImage,
        OutputStream outputStream,
        float quality) throws IOException
    {
        Iterator<ImageWriter> iterator =
            ImageIO.getImageWritersByFormatName("jpg");
        ImageWriter imageWriter = iterator.next();
        ImageWriteParam imageWriteParam = imageWriter.getDefaultWriteParam();
        imageWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
        imageWriteParam.setCompressionQuality(quality);
        ImageOutputStream imageOutputStream =
            new MemoryCacheImageOutputStream(outputStream);
        imageWriter.setOutput(imageOutputStream);
        IIOImage iioimage = new IIOImage(bufferedImage, null, null);
        imageWriter.write(null, iioimage, imageWriteParam);
        imageOutputStream.flush();
    }    
}
Karalynn answered 14/7, 2014 at 21:21 Comment(0)
B
9

After days of research i would prefer javaxt.

use Thejavaxt.io.Image class has a constructor like:

public Image(java.awt.image.BufferedImage bufferedImage)

so you can do (another example):

javaxt.io.Image image = new javaxt.io.Image(bufferedImage);
image.setWidth(50);
image.setOutputQuality(1);

Here's the output:

enter image description here

Beckwith answered 9/11, 2017 at 12:23 Comment(2)
Why is this downvoted? JavaXT uses bilinear interpolation and the output after resize is visually identical to what you get out of Photoshop. I also recommend calling Image.sharpen() after resizing the image.Sigismond
Correction, JavaXT uses area averaging when scaling images and bilinear interpolation when skewing images.Sigismond
F
8

Below are my own implementation of Progressive Scaling, without using any external library. Hope this help.

private static BufferedImage progressiveScaling(BufferedImage before, Integer longestSideLength) {
    if (before != null) {
        Integer w = before.getWidth();
        Integer h = before.getHeight();

        Double ratio = h > w ? longestSideLength.doubleValue() / h : longestSideLength.doubleValue() / w;

        //Multi Step Rescale operation
        //This technique is describen in Chris Campbell’s blog The Perils of Image.getScaledInstance(). As Chris mentions, when downscaling to something less than factor 0.5, you get the best result by doing multiple downscaling with a minimum factor of 0.5 (in other words: each scaling operation should scale to maximum half the size).
        while (ratio < 0.5) {
            BufferedImage tmp = scale(before, 0.5);
            before = tmp;
            w = before.getWidth();
            h = before.getHeight();
            ratio = h > w ? longestSideLength.doubleValue() / h : longestSideLength.doubleValue() / w;
        }
        BufferedImage after = scale(before, ratio);
        return after;
    }
    return null;
}

private static BufferedImage scale(BufferedImage imageToScale, Double ratio) {
    Integer dWidth = ((Double) (imageToScale.getWidth() * ratio)).intValue();
    Integer dHeight = ((Double) (imageToScale.getHeight() * ratio)).intValue();
    BufferedImage scaledImage = new BufferedImage(dWidth, dHeight, BufferedImage.TYPE_INT_RGB);
    Graphics2D graphics2D = scaledImage.createGraphics();
    graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
    graphics2D.drawImage(imageToScale, 0, 0, dWidth, dHeight, null);
    graphics2D.dispose();
    return scaledImage;
}
Filch answered 31/1, 2018 at 10:38 Comment(1)
Wow! I went from illegible text in the scaled image to quite a nice result by using your piece of code. A thousand thanks.Stir
D
5

We should not forget a TwelveMonkeys Library

It contains a really impressive filter collection.

Usage example:

BufferedImage input = ...; // Image to resample
int width, height = ...; // new width/height

BufferedImageOp resampler = new ResampleOp(width, height, ResampleOp.FILTER_LANCZOS);
BufferedImage output = resampler.filter(input, null);
Droit answered 25/6, 2016 at 13:29 Comment(0)
I
2

The result seems to be better (than the result of your program), if you apply Gaussian blur before resizing:

This is the result I get, with sigma * (scale factor) = 0.3:

Thumbnail when bluring first(sigma=15.0)

With ImageJ the code to do this is quite short:

import ij.IJ;
import ij.ImagePlus;
import ij.io.Opener;
import ij.process.ImageProcessor;

public class Resizer {

    public static void main(String[] args) {
        processPicture("X0aPT.jpg", "output.jpg", 0.0198, ImageProcessor.NONE, 0.3);
    }

    public static void processPicture(String inputFile, String outputFilePath, double scaleFactor, int interpolationMethod, double sigmaFactor) {
        Opener opener = new Opener();
        ImageProcessor ip = opener.openImage(inputFile).getProcessor();
        ip.blurGaussian(sigmaFactor / scaleFactor);
        ip.setInterpolationMethod(interpolationMethod);
        ImageProcessor outputProcessor = ip.resize((int)(ip.getWidth() * scaleFactor), (int)(ip.getHeight()*scaleFactor));
        IJ.saveAs(new ImagePlus("", outputProcessor), outputFilePath.substring(outputFilePath.lastIndexOf('.')+1), outputFilePath);
    }

}

BTW: You only need ij-1.49d.jar (or equivalent for other version); there's no need to install ImageJ.

Increase answered 14/7, 2014 at 22:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.