Java - redimensionner l'image sans perte de qualité


J'ai 10 000 photos qui doivent être redimensionnées donc, j'ai un programme Java pour le faire. Malheureusement, la qualité de l'image est mal perdue et je n'ai pas accès aux images non compressées.

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;
}

Une image avec laquelle je travaille est la suivante: Firwork-original-grand

C'est le redimensionnement manuel que j'ai fait dans Microsoft Paint:

redimensionner-à l'aide de peinture-petit

Et c'est la sortie de mon programme [bilinéaire]:

redimensionner-utilisation du programme java-petit

MISE À JOUR: Aucune différence significative en utilisant BICUBIC

Et c'est la sortie de mon programme [bicubique]:

entrez la description de l'image ici

Existe-t-il de toute façon pour augmenter la qualité de la sortie du programme afin que je n'aie pas à redimensionner manuellement toutes les photos?

Merci d'avance!

Author: user3362954, 2014-07-15

7 answers

Malheureusement, il n'y a pas de mise à l'échelle prête à l'emploi recommandée en Java qui fournit de bons résultats visuellement. Entre autres, voici les méthodes que je recommande pour la mise à l'échelle:

  • Rééchantillonnage Lanczos3 (généralement visuellement meilleur, mais plus lent)
  • Mise à l'échelle progressive (généralement visuellement fine, peut être assez rapide)
  • Mise à l'échelle en une étape pour la mise à l'échelle (avec Graphics2d résultats rapides et bons bicubiques, généralement pas aussi bons que Lanczos3)

Exemples pour chaque méthode peut être trouvé dans cette réponse.

Comparaison Visuelle

Voici votre image mise à l'échelle à 96x140 avec différentes méthodes/bibliothèques. Cliquez sur l'image pour obtenir la taille complète:

comparaison

comparaison de zoom

  1. Morten Nobel de la lib Lanczos3
  2. Thumbnailator Bilinéaire Progressive Mise À L'Échelle
  3. Imgscalr ULTRA_QUALTY (mise à l'échelle progressive Bicubique en 1/7 étapes)
  4. Qualité Imgscalr (1/2 étape Bicubique progressive Mise à l'échelle)
  5. Mise à l'échelle progressive bilinéaire lib de Morten Nobel
  6. Graphics2d Interpolation bicubique
  7. Graphics2d Interpolation du voisin le plus proche
  8. Photoshop CS5 bicubic comme référence

Malheureusement, une seule image ne suffit pas pour juger un algorithme de mise à l'échelle, vous devez tester des icônes avec des arêtes vives, des photos avec du texte, etc.

Rééchantillonnage de Lanczos

Est dit bon pour la montée en gamme et surtout la réduction d'échelle. Malheureusement il n'y a pas implémentation native dans le JDK actuel donc vous l'implémentez vous-même et utilisez une lib comme la lib de Morten Nobel. Un exemple simple utilisant ladite lib:

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

La lib est publiée sur maven-central qui n'est malheureusement pas mentionnée. L'inconvénient est qu'il est généralement très lent sans implémentations hautement optimisées ou accélérées par le matériel connues de moi. L'implémentation de Nobel est environ 8 fois plus lente qu'un algorithme de mise à l'échelle progressive en 1/2 étape avec Graphics2d. En savoir plus sur cette lib sur son blog .

Mise à l'échelle progressive

Mentionné dans Le blog de Chris Campbell sur la mise à l'échelle en Java, la mise à l'échelle progressive consiste essentiellement à mettre à l'échelle une image par étapes plus petites jusqu'à ce que les dimensions finales soient atteintes. Campbell le décrit comme réduisant de moitié la largeur / hauteur jusqu'à ce que vous atteigniez la cible. Cela produit de bons résultats et peut être utilisé avec {[10] } qui peut être accéléré par le matériel, donc ayant généralement très bon performance avec des résultats acceptables dans la plupart des cas. Le principal inconvénient de ceci est si réduit de moins de la moitié en utilisant Graphics2D fournit les mêmes résultats médiocres car il n'est mis à l'échelle qu'une seule fois.

Voici un exemple simple sur son fonctionnement:

mise à l'échelle progressive

Les bibliothèques suivantes incorporent des formes de mise à l'échelle progressive basées sur Graphics2d:

Thumbnailator v0.4.8

Utilise l'algorithme bilinéaire progressif si la cible est au moins la moitié de chaque dimension, sinon il utilise une mise à l'échelle bilinéaire simple Graphics2d et bicubique pour la mise à l'échelle.

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);

Il est aussi rapide ou légèrement plus rapide que la mise à l'échelle en une étape avec Graphics2dmarquant une moyenne de 6,9 secondes dans mon benchmark.

Imgscalr v4.2

Utilise une mise à l'échelle bicubique progressive. Dans le paramètre QUALITY, il utilise l'algorithme de style Campbell avec une réduction de moitié des dimensions à chaque étape tandis que le ULTRA_QUALITY a des étapes plus fines, réduisant la taille à chaque incrément de 1/7 ce qui génère des images généralement plus douces mais minimise les cas où seulement 1 itération est utilisée.

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

Le principal inconvénient est la performance. ULTRA_QUALITY est considérablement plus lent que les autres libs. Même QUALITY un peu plus lent que l'implémentation de Thumbnailator. Mon simplebenchmark a donné lieu à 26.2 sec et 11.1 sec de moyenne respectivement.

Morten Nobel de la lib v0.8.6

A également des implémentations pour la mise à l'échelle progressive pour tous les basic Graphics2d (bilinéaire, bicubique et voisin le plus proche)

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

Un mot sur les méthodes de mise à l'échelle JDK

La méthode jdk actuelle pour redimensionner une image serait quelque chose comme ceci

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();

Mais la plupart sont très déçus par le résultat de la réduction d'échelle, quelle que soit l'interpolation ou autre RenderHints utilisée. D'autre part, la mise à l'échelle semble produire des images acceptables (le meilleur serait bicubique). Dans la version JDK précédente (nous parlons 90s v1.1) Image.getScaledInstance() a été introduit qui a fourni de bons résultats visuels avec paramètre SCALE_AREA_AVERAGINGmais vous êtes découragé de l'utiliser - lisez l'explication complète ici.

 44
Author: for3st, 2018-02-03 19:05:04

Thumbnailator est une bibliothèque qui a été écrite pour créer des vignettes de haute qualité de manière simple, et effectuer une conversion par lots d'images existantes est l'un de ses cas d'utilisation.

Effectuer un redimensionnement par lots

Par exemple, pour adapter votre exemple en utilisant Thumbnailator, vous devriez pouvoir obtenir des résultats similaires avec le code suivant:

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

Cela va aller de l'avant et prend tous les fichiers dans votre répertoire images et procéder à les traiter un par un, essayez de les redimensionner pour s'adapter aux dimensions de 112 x 75, et il va tenter de conserver les proportions de l'image d'origine pour éviter la "déformation" de l'image.

Thumbnailator ira de l'avant et lira tous les fichiers, quels que soient les types d'image (tant que l'image Java IO prend en charge le format, Thumbnailator le traitera), effectuera l'opération de redimensionnement et produira les vignettes sous forme de fichiers JPEG, tout en collant un thumbnail. au début du nom du fichier.

Ce qui suit est un illustration de la façon dont le nom de fichier de l'original sera utilisé dans le nom de fichier de la vignette si le code ci-dessus est exécuté.

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

Générer des vignettes de haute qualité

En termes de qualité d'image, comme mentionné dans réponse de Marco13, la technique décrite par Chris Campbell dans son Les dangers de l'image.getScaledInstance () est implémenté dans Thumbnailator, ce qui permet d'obtenir des vignettes de haute qualité sans nécessiter de traitement compliqué.

Le voici la vignette générée lors du redimensionnement de l'image de feux d'artifice affichée dans la question d'origine à l'aide de Thumbnailator:

Vignette de l'image dans la question originale

L'image ci-dessus a été créée avec le code suivant:

BufferedImage thumbnail = 
    Thumbnails.of(new URL("http://i.stack.imgur.com/X0aPT.jpg"))
        .height(75)
        .asBufferedImage();

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

Le code montre qu'il peut également accepter des URL en entrée, et que Thumbnailator est également capable de créer des BufferedImage.


Avertissement: Je suis le mainteneur de la bibliothèque Thumbnailator .

 32
Author: coobird, 2017-05-23 11:47:11

Compte tenu de votre image d'entrée, la méthode de la réponse dans le premier lien dans les commentaires (bravo à Chris Campbell) produit l'une des vignettes suivantes:

entrez la description de l'image icientrez la description de l'image ici

(L'autre est la vignette que vous avez créée avec MS Paint. Il est difficile d'appeler l'un d'entre eux "mieux" que l'autre...)

EDIT: Juste pour signaler ainsi: Le principal problème avec votre code d'origine est que vous n'avez pas vraiment échelle l'image en plusieurs étapes. Vous venez d'utiliser une boucle étrange pour "calculer" la taille cible. Le point clé est que vous effectuez réellement la mise à l'échelle en plusieurs étapes.

Juste pour être complet, le MVCE

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();
    }    
}
 15
Author: Marco13, 2014-07-14 21:21:45

Il ne faut pas oublier une bibliothèque TwelveMonkeys

Il contient une collection de filtres vraiment impressionnante.

Exemple d'Utilisation:

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);
 3
Author: Quark, 2016-06-25 13:29:22

Après des jours de recherche, je préférerais javaxt.

Utiliser Lejavaxt.io.Image la classe dispose d'un constructeur comme:

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

, de Sorte que vous pouvez faire (another example):

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

Voici la sortie:

entrez la description de l'image ici

 2
Author: MaMaRo N0, 2017-11-17 17:23:55

, Le résultat semble être mieux (que le résultat de votre programme), si vous appliquez flou Gaussien avant de redimensionner:

C'est le résultat que j'obtiens, avec sigma * (scale factor) = 0.3:

Vignette lors du brouillage en premier (sigma=15.0)

Avec ImageJ le code pour ce faire est assez court:

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: Vous avez seulement besoin de ij-1.49d.jar (ou équivalent pour une autre version); il n'est pas nécessaire de installer ImageJ.

 1
Author: fabian, 2014-07-14 23:04:44

Voici ma propre implémentation de la mise à l'échelle progressive, sans utiliser de bibliothèque externe. Espérons que cette aide.

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;
}
 1
Author: Maxwell Cheng, 2018-01-31 10:38:27