web-dev-qa-db-fra.com

Java - redimensionner l'image sans perte de qualité

J'ai 10 000 photos qui doivent être redimensionnées, donc j'ai un programme Java). 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 - large

Voici le redimensionnement manuel que j'ai effectué dans Microsoft Paint:

resize - using Paint - small

et voici le résultat de mon programme [bilinéaire]:

resize - using Java program - small

PDATE: Pas de différence significative en utilisant BICUBIC

et voici le résultat de mon programme [bicubic]:

enter image description here

y a-t-il un moyen d'augmenter la qualité de la sortie du programme pour ne pas avoir à redimensionner manuellement toutes les photos?

Merci d'avance!

54
user3362954

Malheureusement, il n'y a pas de mise à l'échelle prête à l'emploi recommandée dans Java) qui fournit des résultats visuellement satisfaisants. Voici, entre autres, les méthodes que je recommande pour la mise à l'échelle:

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

Des exemples pour chaque méthode peuvent être trouvés dans cette réponse.

Comparaison visuelle

Voici votre image redimensionnée à 96x140 Avec différentes méthodes/libs. Cliquez sur l'image pour obtenir la taille complète:

comparison

comparison zoom

  1. Liben Lanczos3 de Morten Nobel
  2. Thumbnailator Mise à l'échelle progressive bilinéaire
  3. Imgscalr ULTRA_QUALTY (Mise à l'échelle progressive bicubique en 1/7)
  4. Imgscalr QUALTY (Mise à l'échelle progressive bicubique 1/2 étape)
  5. Morten Nobel lib Bilinear Progressive Scaling
  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 redimensionnement, vous devez tester les icônes aux contours nets, les photos avec du texte, etc.

Rééchantillonnage Lanczos

Est dit être bon pour le haut et surtout la réduction d'échelle. Malheureusement, il n'y a pas d'implémentation native dans le JDK actuel vous devez donc l'implémenter vous-même et utiliser une bibliothèque telle que la bibliothèque de Morten Nobel . Un exemple simple utilisant dit 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 aucune implémentation hautement optimisée ou à accélération matérielle que je connaisse. L'implémentation de Nobel est environ 8 fois plus lente qu'un algorithme de mise à l'échelle progressive avec 1/2 étape avec Graphics2d. En savoir plus sur cette lib sur son blog .

Mise à l'échelle progressive

Mentionné dans blog de Chris Campbell sur le redimensionnement en Java, le redimensionnement progressif consiste à redimensionner progressivement une image par incréments jusqu'à atteindre les dimensions définitives. Campbell le décrit comme divisant par deux la largeur/hauteur jusqu'à atteindre la cible. Cela produit de bons résultats et peut être utilisé avec Graphics2D, Qui peut être accéléré matériellement. Par conséquent, il offre généralement de très bonnes performances avec des résultats acceptables dans la plupart des cas. L'inconvénient majeur de ceci est que, même si la réduction d'échelle est inférieure à la moitié, l'utilisation de Graphics2D Fournit les mêmes résultats médiocres puisqu'elle n'est réduite qu'une seule fois.

Voici un exemple simple sur la façon dont cela fonctionne:

progressive scaling

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 représente au moins la moitié de chaque dimension. Sinon, il utilise une simple mise à l'échelle bilinéaire Graphics2d Et bicubique pour la conversion ascendante.

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 Graphics2d Marquant en moyenne 6,9 ​​secondes sur un repère .

Imgscalr v4.2

Utilise une mise à l'échelle bicubique progressive. Dans le paramètre QUALITY, il utilise un algorithme de style Campbell avec une réduction de moitié des dimensions à chaque étape, tandis que le paramètre ULTRA_QUALITY Comporte des étapes plus fines, réduisant la taille de chaque incrément de 1/7, ce qui génère des images généralement plus douces tout en réduisant les occurrences. 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 bibliothèques. Même QUALITY un peu plus lent que l'implémentation de Thumbnailator. Mon simple repère a abouti à une moyenne de 26,2 secondes et de 11,1 secondes respectivement.

lib v0.8.6 de Morten Nobel

Possède également des implémentations pour une mise à l'échelle progressive de tous les éléments de base Graphics2d (Bilinéaire, bicubique et le plus proche voisin)

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

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

JDK actuelle pour redimensionner une image serait quelque chose comme ça

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 du résultat de la réduction d'échelle, peu importe l'interpolation ou les autres RenderHints utilisés. D'autre part, la conversion ascendante semble produire des images acceptables (le mieux serait bicubique). Dans la version précédente de JDK (nous parlons de 90s v1.1), Image.getScaledInstance() a été introduit. Il fournit de bons résultats visuels avec le paramètre SCALE_AREA_AVERAGING, Mais vous êtes déconseillé de l'utiliser - lisez l'explication complète ici .

62
patrickf

Thumbnailator est une bibliothèque qui a été écrite pour créer des vignettes de haute qualité de manière simple. La 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 à l'aide de 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 de votre répertoire images et les traiter un par un, essayez de les redimensionner pour les adapter à la dimension de 112 x 75, et il tentera de préserver les proportions de l'image d'origine pour éviter le "gauchissement" de l'image.

Thumbnailator continue et lit tous les fichiers, quel que soit le type d’image (tant que le Java Image IO prend en charge le format, Thumbnailator le traite)), effectue l'opération de redimensionnement et afficher les vignettes sous forme de fichiers JPEG, tout en ajoutant un thumbnail. au début du nom de fichier.

Voici une 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 Marco1 , la technique décrite par Chris Campbell dans son Les dangers de Image.getScaledInstance () est implémentée dans Thumbnailator, ce qui entraîne une Des vignettes de qualité sans nécessiter de traitement compliqué.

La vignette suivante est générée lors du redimensionnement de l'image Fireworks affichée dans la question d'origine à l'aide de Thumbnailator:

Thumbnail of image in original question

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 les URL comme entrée et que Thumbnailator est également capable de créer BufferedImages.


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

37
coobird

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

enter image description hereenter image description here

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

EDIT: Juste pour le souligner aussi: le problème principal avec votre code original était 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 le 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();
    }    
}
16
Marco13

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

utilisez le javaxt.io.Image la classe a un constructeur comme:

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

afin que vous puissiez faire ( another example ):

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

Voici la sortie:

enter image description here

5
MaMaRo N0

Nous ne devrions pas oublier a TwelveMonkeys Library

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

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

Voici le résultat obtenu avec sigma * (scale factor) = 0.3:

Thumbnail when bluring first(sigma=15.0)

Avec ImageJ le code à 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'y a pas besoin de installer ImageJ.

1
fabian

Ci-dessous, ma propre implémentation de Progressive Scaling, sans utiliser aucune bibliothèque externe. J'espère 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
Maxwell Cheng