it-swarm.com.ru

Java - изменение размера изображения без потери качества

У меня есть 10000 фотографий, размер которых нужно изменить, поэтому у меня есть программа Java для этого. К сожалению, качество изображения плохо теряется, и у меня нет доступа к несжатым изображениям.

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

Изображение, с которым я работаю, таково: Firwork - original - large

Это ручное изменение размера, которое я сделал в Microsoft Paint:

resize - using Paint - small

и это вывод из моей программы [билинейный]:

resize - using Java program - small

ОБНОВЛЕНИЕ: Нет значительных различий при использовании BICUBIC

и это вывод из моей программы [бикубической]:

enter image description here

в любом случае можно ли повысить качество вывода программы, чтобы мне не пришлось вручную изменять размер всех фотографий?

Заранее спасибо!

54
user3362954

К сожалению, в Java нет рекомендуемого готового масштабирования, которое обеспечивает визуально хорошие результаты. Среди прочего, вот методы, которые я рекомендую для масштабирования:

  • Lanczos3 Resampling (обычно визуально лучше, но медленнее)
  • Прогрессивное масштабирование вниз (обычно визуально хорошо, может быть довольно быстро)
  • Одноступенчатое масштабирование для масштабирования с повышением (с Graphics2d бикубическим быстрым и хорошими результатами, обычно не такими хорошими, как у Lanczos3)

Примеры для каждого метода можно найти в этом ответе.

Визуальное сравнение

Вот ваше изображение, масштабированное до 96x140 различными методами/библиотеками. Нажмите на изображение, чтобы получить полный размер:

comparison

comparison zoom

  1. Lib Мортена Нобеля Lanczos3
  2. Thumbnailator Bilinear Progressive Scaling
  3. Imgscalr ULTRA_QUALTY (Бикубическое прогрессивное масштабирование 1/7 шага)
  4. Imgscalr КАЧЕСТВО (1/2 шага бикубического прогрессивного масштабирования)
  5. Билинейная прогрессивная шкала Мортена Нобеля
  6. Graphics2d бикубическая интерполяция
  7. Graphics2d Ближайшая соседняя интерполяция
  8. Photoshop CS5 Bicubic в качестве эталона

К сожалению, одного изображения недостаточно, чтобы судить об алгоритме масштабирования, вы должны проверить иконки с острыми краями, фотографии с текстом и т.д.

Lanczos Resampling

Говорят, что это хорошо для повышения и особенно уменьшения масштаба. К сожалению в текущей версии JDK нет нативной реализации , поэтому вы можете либо реализовать ее самостоятельно, либо использовать библиотеку типа библиотека Мортена Нобеля . Простой пример с использованием указанной библиотеки:

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

Библиотека опубликована на maven-central , к сожалению, она не упоминается. Недостатком является то, что он обычно очень медленный без каких-либо хорошо оптимизированных или аппаратно ускоренных реализаций, известных мне. Реализация Нобеля примерно в 8 раз медленнее, чем алгоритм прогрессивного масштабирования с шагом 1/2 с Graphics2d. Подробнее об этой библиотеке читайте в его блоге .

Прогрессивное масштабирование

Упоминается в блог Криса Кэмпбелла о масштабировании в Java, прогрессивное масштабирование в основном заключается в постепенном масштабировании изображения меньшими шагами, пока не будут достигнуты окончательные размеры. Кэмпбелл описывает его как половину ширины/высоты, пока вы не достигнете цели. Это дает хорошие результаты и может использоваться с Graphics2D, который может быть аппаратно ускорен, поэтому обычно имеет очень хорошую производительность с приемлемыми результатами в большинстве случаев. Основным недостатком этого является то, что при уменьшении менее половины с использованием Graphics2D получаются те же посредственные результаты, поскольку он масштабируется только один раз.

Вот простой пример того, как это работает:

progressive scaling

Следующие библиотеки включают формы прогрессивного масштабирования на основе Graphics2d:

Thumbnailator v0.4.8

Использует прогрессивный билинейный алгоритм, если целью является как минимум половина каждого измерения, в противном случае он использует простое билинейное масштабирование Graphics2d и бикубическое для масштабирования.

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

Это так же быстро или немного быстрее, чем одношаговое масштабирование с Graphics2d, набирая в среднем 6,9 секунды в моем тест .

Imgscalr v4.2

Использует прогрессивное бикубическое масштабирование. В настройке QUALITY он использует алгоритм стиля Кэмпбелла с уменьшением вдвое размеров каждого шага, в то время как ULTRA_QUALITY имеет более мелкие шаги, уменьшая размер каждого приращения на 1/7, что генерирует обычно более мягкие изображения, но минимизирует случаи, когда используется только 1 итерация.

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

Основным недостатком является производительность. ULTRA_QUALITY значительно медленнее, чем другие библиотеки. Даже QUALITY немного медленнее, чем реализация Thumbnailator. Мой простой тест привел в среднем к 26,2 с и 11,1 с соответственно.

lib Мортена Нобеля v0.8.6

Имеет также реализации для прогрессивного масштабирования для всех базовых Graphics2d (билинейный, бикубический и ближайший сосед)

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

Слово о методах масштабирования JDK

Текущий JDK способ масштабирования изображения будет примерно таким

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

но большинство очень разочарованы результатом уменьшения масштаба независимо от того, какая интерполяция или другое RenderHints используются. С другой стороны, апскейлинг производит приемлемые изображения (лучше всего бикубическое). В предыдущей версии JDK (мы говорим о 90-х v1.1) была введена функция Image.getScaledInstance(), которая обеспечивала хорошие визуальные результаты с параметром SCALE_AREA_AVERAGING, но вам не рекомендуется его использовать - полное объяснение здесь .

59
patrickf

Thumbnailator - это библиотека, которая была написана для простого создания высококачественных миниатюр, и пакетное преобразование существующих изображений - один из ее вариантов использования.

Выполнение пакетного изменения размера

Например, чтобы адаптировать ваш пример с помощью Thumbnailator, вы должны достичь аналогичных результатов с помощью следующего кода:

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

При этом все файлы в вашем каталоге images будут продолжены, и они будут обрабатывать их один за другим, пытаться изменить их размер в соответствии с размерами 112 x 75, и попытаться сохранить соотношение сторон исходного изображения, чтобы предотвратить "Деформация" изображения.

Thumbnailator продолжит читать все файлы, независимо от типов изображений (если Java Image IO поддерживает формат, Thumbnailator будет обрабатывать его), выполнит операцию изменения размера и выведет миниатюры в виде файлов JPEG, добавляя thumbnail. в начало имени файла.

Ниже приведена иллюстрация того, как имя файла оригинала будет использоваться в имени файла пиктограммы, если приведенный выше код выполняется.

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

Генерация высококачественных миниатюр

С точки зрения качества изображения, как упоминалось в ответ Marco1 , метод, описанный Крисом Кэмпбеллом в его Опасности Image.getScaledInstance () , реализован в Thumbnailator, что приводит к высокой Качественные миниатюры, не требующие сложной обработки.

Ниже приведен эскиз, созданный при изменении размера изображения фейерверка, показанного в исходном вопросе с помощью Thumbnailator:

Thumbnail of image in original question

Изображение выше было создано с помощью следующего кода:

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

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

Код показывает, что он также может принимать URL-адреса в качестве входных данных и что Thumbnailator также способен создавать BufferedImages.


Отказ от ответственности: я поддерживаю библиотеку Thumbnailator .

36
coobird

Учитывая ваше входное изображение, метод из ответа в первой ссылке в комментариях (слава Крису Кэмпбеллу) создает одну из следующих миниатюр:

enter image description hereenter image description here

(Другой - это миниатюра, которую вы создали с помощью MS Paint. Трудно назвать один из них "лучше", чем другой ...)

Правка: Просто чтобы указать на это: Основная проблема с вашим исходным кодом в том, что вы на самом деле не масштабировать изображение в несколько шагов. Вы просто использовали странный цикл, чтобы "вычислить" целевой размер. Ключевым моментом является то, что вы фактически выполняете масштабирование в несколько шагов.

Просто для полноты, 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

После нескольких дней исследований я бы предпочел javaxt.

используйте javaxt.io.Image класс имеет конструктор как:

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

так что вы можете сделать ( another example ):

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

Вот вывод:

enter image description here

5
MaMaRo N0

Мы не должны забывать Библиотека TwelveMonkeys

Он содержит действительно впечатляющую коллекцию фильтров.

Пример использования:

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

Результат будет лучше (чем результат вашей программы), если вы примените размытие по Гауссу перед изменением размера:

Вот результат, который я получаю, с sigma * (scale factor) = 0.3:

Thumbnail when bluring first(sigma=15.0)

С ImageJ код для этого довольно короткий:

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

}

Кстати: вам нужен только ij-1.49d.jar (или эквивалент для другой версии); нет необходимости устанавливать ImageJ.

1
fabian

Ниже приведены мои собственные реализации прогрессивного масштабирования без использования какой-либо внешней библиотеки. Надеюсь, это поможет.

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