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 - оригинал - большой

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

изменить размер - используя Paint - маленький

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

изменить размер - с помощью Java-программы - маленький

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

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

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

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

7 ответов

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

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

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

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

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

сравнение

сравнительный зум

  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 дает такие же посредственные результаты, поскольку он масштабируется только один раз.

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

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

Следующие библиотеки включают в себя формы прогрессивного масштабирования на основе 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 но вы не можете использовать его - прочитайте полное объяснение здесь.

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 поддерживает формат, Thumbnailator будет обрабатывать его), выполнит операцию изменения размера и выведет миниатюры в виде файлов JPEG, а затем сохранит thumbnail. в начало имени файла.

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

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

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

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

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

Миниатюра изображения в оригинальном вопросе

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

BufferedImage thumbnail = 
    Thumbnails.of(new URL("https://stackru.com/images/8b6778aa369b946185a7ee31387cb808ccaa89f0.jpg"))
        .height(75)
        .asBufferedImage();

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

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


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

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

(Другой - это миниатюра, которую вы создали с помощью 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();
    }    
}

После нескольких дней исследований я бы предпочел 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);

Вот вывод:

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

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

Мы не должны забывать библиотеку 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);

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

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

Пиктограмма при первом размытии (сигма = 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.

Другие вопросы по тегам