Как масштабировать BufferedImage

Следуя javadocs, я безуспешно пытался масштабировать BufferedImage вот мой код:

BufferedImage image = MatrixToImageWriter.getBufferedImage(encoded);
Graphics2D grph = image.createGraphics();
grph.scale(2.0, 2.0);
grph.dispose();

Я не могу понять, почему это не работает, любая помощь?


person Thiago Diniz    schedule 18.11.2010    source источник
comment
Отличный учебник: glyphic.com/transform/applet/1intro.html   -  person Marcus Becker    schedule 29.01.2015
comment
На момент написания этой статьи самым популярным ответом был неправильный ответ. Он масштабирует изображение, но возвращает изображение того же размера, но отсутствует 3/4 изображения. Это ответ, данный trashgod. Это близко, но есть небольшая ошибка.   -  person MiguelMunoz    schedule 28.09.2017
comment
Спасибо, @MiguelMunoz. Поскольку у нас есть больше отзывов, я могу изменить ответ.   -  person Thiago Diniz    schedule 11.05.2019


Ответы (7)


AffineTransformOp обеспечивает дополнительную гибкость выбора тип интерполяции.

BufferedImage before = getBufferedImage(encoded);
int w = before.getWidth();
int h = before.getHeight();
BufferedImage after = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale(2.0, 2.0);
AffineTransformOp scaleOp = 
   new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
after = scaleOp.filter(before, after);

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

person trashgod    schedule 18.11.2010
comment
Действительно ли необходимо выделять всю память для after, когда у вас есть оператор вроде: after = ...? - person Martijn Courteaux; 18.07.2011
comment
@Martijn: это зависит от того, какой ColorModel вы хотите в filter(). Он возвращает ссылку, поэтому лишней памяти нет. - person trashgod; 18.07.2011
comment
Есть ли способ сделать это с желаемой шириной и высотой вместо масштабного коэффициента? - person Drazen Bjelovuk; 26.09.2013
comment
Да, параметры шкалы — это просто отношения нового к старому для x и y соответственно; оставьте их равными, чтобы сохранить соотношение сторон. - person trashgod; 26.09.2013
comment
Насколько это быстро? Мне нужны быстрые вычисления для компьютерного зрения. - person Tomáš Zato - Reinstate Monica; 27.01.2015
comment
@TomášZato: вам может потребоваться профилировать репрезентативный образец. - person trashgod; 27.01.2015
comment
Небольшая поправка, потому что 1 строка лучше, чем 2... AffineTransform at = AffineTransform.getScaleInstance(2.0, 2.0); - person xtempore; 27.11.2015
comment
Я не пробовал этот код, но я не уверен, что он будет работать. Вы создаете BufferedImage, вызываемый после того, как он имеет те же размеры, что и оригинал. Возможно, вам потребуется масштабировать размеры нового изображения. Но, опять же, я не уверен в этом. scaleOp.filter() может позаботиться об этом за вас. - person MiguelMunoz; 11.09.2017
comment
Я только что проверил это. Как я и подозревал, after имеет тот же размер, и это просто верхняя левая четверть оригинала. Исправление заключается в умножении w и h на масштаб при создании after. - person MiguelMunoz; 14.09.2017
comment
@MiguelMunoz, этот код довольно старый, я не проверял ваш ответ, позвольте мне получить больше отзывов, и тогда я смогу изменить ответ. - person Thiago Diniz; 11.05.2019
comment
@ThiagoDiniz: Фрагмент минимальный, но правильный; этот связанный ответ иллюстрирует повторную выборку результата для оптимизации обрезки. - person trashgod; 11.05.2019

К сожалению, производительность getScaledInstance() очень низкая, если не проблематичная.

Альтернативный подход — создать новый BufferedImage и нарисовать масштабированную версию оригинала на новом.

BufferedImage resized = new BufferedImage(newWidth, newHeight, original.getType());
Graphics2D g = resized.createGraphics();
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
    RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g.drawImage(original, 0, 0, newWidth, newHeight, 0, 0, original.getWidth(),
    original.getHeight(), null);
g.dispose();

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

int newWidth = new Double(original.getWidth() * widthFactor).intValue();
int newHeight = new Double(original.getHeight() * heightFactor).intValue();

EDIT: нашел статью, иллюстрирующую проблему с производительностью: Опасности Image.getScaledInstance ()

person charisis    schedule 18.11.2010
comment
Я думаю, что getScaledInstance() в настоящее время работает быстрее, по крайней мере, если у вас есть приличная видеокарта, благодаря оптимизированному конвейеру рендеринга Java2D. - person ceklock; 24.02.2014
comment
К вашему сведению, см. здесь другие возможные значения для RenderingHints.KEY_INTERPOLATION - person rustyx; 03.11.2014
comment
Работал как шарм! - person RafiAlhamd; 27.10.2020
comment
Это мне очень помогло, не меняя библиотеку - person Maia; 26.03.2021

Использование imgscalr — библиотеки масштабирования изображений Java:

BufferedImage image =
     Scalr.resize(originalImage, Scalr.Method.BALANCED, newWidth, newHeight);

Это достаточно быстро для меня.

person ceklock    schedule 23.02.2014
comment
Согласитесь, это лучшее решение, позволяющее избежать всех проблем с прозрачностью, неправильными переводами, неправильными цветами и т. д. при использовании аффинного преобразования и различных других методов. - person zyamys; 02.03.2014
comment
Потрясающий! Первое решение, которое я видел в этой теме, которое дало мне то, что мне нужно. - person Shadoninja; 02.04.2016

Как говорит @Bozho, вы, вероятно, захотите использовать getScaledInstance.

Однако, чтобы понять, как работает grph.scale(2.0, 2.0), вы можете взглянуть на этот код:

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;

import javax.imageio.ImageIO;
import javax.swing.ImageIcon;


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

        final int SCALE = 2;

        Image img = new ImageIcon("duke.png").getImage();

        BufferedImage bi = new BufferedImage(SCALE * img.getWidth(null),
                                             SCALE * img.getHeight(null),
                                             BufferedImage.TYPE_INT_ARGB);

        Graphics2D grph = (Graphics2D) bi.getGraphics();
        grph.scale(SCALE, SCALE);

        // everything drawn with grph from now on will get scaled.

        grph.drawImage(img, 0, 0, null);
        grph.dispose();

        ImageIO.write(bi, "png", new File("duke_double_size.png"));
    }
}

Учитывая duke.png:
введите здесь описание изображения

он создает duke_double_size.png:
введите здесь описание изображения

person aioobe    schedule 18.11.2010
comment
Я попробовал этот код, но я не получил показанного результата. Результат, который я получил, был гораздо более сильно искажен. Если вы увеличите первое изображение в своем браузере до тех пор, пока оно не станет примерно того же размера, что и второе, вы получите лучшее представление о том, что производит этот код. (Я пытался поместить полученное изображение в этот комментарий, но это не сработало. Я думаю, изображения не разрешены в комментариях.) - person MiguelMunoz; 28.09.2017
comment
Не могли бы вы попробовать grph.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolation);, где interpolation равно RenderingHints.VALUE_INTERPOLATION_..., например VALUE_INTERPOLATION_BICUBIC? - person aioobe; 28.09.2017

Чтобы масштабировать изображение, вам нужно создать новое изображение и рисовать в нем. Один из способов — использовать метод filter() для AffineTransferOp, как предлагается здесь. Это позволяет выбрать метод интерполяции.

private static BufferedImage scale1(BufferedImage before, double scale) {
    int w = before.getWidth();
    int h = before.getHeight();
    // Create a new image of the proper size
    int w2 = (int) (w * scale);
    int h2 = (int) (h * scale);
    BufferedImage after = new BufferedImage(w2, h2, BufferedImage.TYPE_INT_ARGB);
    AffineTransform scaleInstance = AffineTransform.getScaleInstance(scale, scale);
    AffineTransformOp scaleOp 
        = new AffineTransformOp(scaleInstance, AffineTransformOp.TYPE_BILINEAR);

    scaleOp.filter(before, after);
    return after;
}

Другой способ — просто нарисовать исходное изображение в новом изображении, используя операцию масштабирования для выполнения масштабирования. Этот метод очень похож, но он также иллюстрирует, как вы можете нарисовать все, что захотите, на конечном изображении. (Я ставлю пустую строку там, где два метода начинают различаться.)

private static BufferedImage scale2(BufferedImage before, double scale) {
    int w = before.getWidth();
    int h = before.getHeight();
    // Create a new image of the proper size
    int w2 = (int) (w * scale);
    int h2 = (int) (h * scale);
    BufferedImage after = new BufferedImage(w2, h2, BufferedImage.TYPE_INT_ARGB);
    AffineTransform scaleInstance = AffineTransform.getScaleInstance(scale, scale);
    AffineTransformOp scaleOp 
        = new AffineTransformOp(scaleInstance, AffineTransformOp.TYPE_BILINEAR);

    Graphics2D g2 = (Graphics2D) after.getGraphics();
    // Here, you may draw anything you want into the new image, but we're
    // drawing a scaled version of the original image.
    g2.drawImage(before, scaleOp, 0, 0);
    g2.dispose();
    return after;
}

Дополнение: результаты

Чтобы проиллюстрировать различия, я сравнил результаты пяти приведенных ниже методов. Вот как выглядят результаты в увеличенном и уменьшенном масштабе вместе с данными о производительности. (Производительность варьируется от одного прогона к другому, поэтому принимайте эти цифры только как приблизительные ориентиры.) Верхнее изображение — оригинал. Я масштабирую его в двойном и половинном размере.

Как видите, AffineTransformOp.filter(), используемый в scaleBilinear(), быстрее, чем стандартный метод рисования Graphics2D.drawImage() в scale2(). Также интерполяция BiCubic является самой медленной, но дает наилучшие результаты при расширении изображения. (По производительности его следует сравнивать только с scaleBilinear() и scaleNearest().) Билинейный режим кажется лучше для сжатия изображения, хотя это сложный выбор. А NearestNeighbor — самый быстрый, с худшими результатами. Билинейность кажется лучшим компромиссом между скоростью и качеством. Image.getScaledInstance(), вызванный в методе questionable(), работал очень плохо и возвращал то же самое низкое качество, что и NearestNeighbor. (Числа производительности даны только для расширения изображения.)

введите описание изображения здесь

public static BufferedImage scaleBilinear(BufferedImage before, double scale) {
    final int interpolation = AffineTransformOp.TYPE_BILINEAR;
    return scale(before, scale, interpolation);
}

public static BufferedImage scaleBicubic(BufferedImage before, double scale) {
    final int interpolation = AffineTransformOp.TYPE_BICUBIC;
    return scale(before, scale, interpolation);
}

public static BufferedImage scaleNearest(BufferedImage before, double scale) {
    final int interpolation = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
    return scale(before, scale, interpolation);
}

@NotNull
private static 
BufferedImage scale(final BufferedImage before, final double scale, final int type) {
    int w = before.getWidth();
    int h = before.getHeight();
    int w2 = (int) (w * scale);
    int h2 = (int) (h * scale);
    BufferedImage after = new BufferedImage(w2, h2, before.getType());
    AffineTransform scaleInstance = AffineTransform.getScaleInstance(scale, scale);
    AffineTransformOp scaleOp = new AffineTransformOp(scaleInstance, type);
    scaleOp.filter(before, after);
    return after;
}

/**
 * This is a more generic solution. It produces the same result, but it shows how you 
 * can draw anything you want into the newly created image. It's slower 
 * than scaleBilinear().
 * @param before The original image
 * @param scale The scale factor
 * @return A scaled version of the original image
 */
private static BufferedImage scale2(BufferedImage before, double scale) {
    int w = before.getWidth();
    int h = before.getHeight();
    // Create a new image of the proper size
    int w2 = (int) (w * scale);
    int h2 = (int) (h * scale);
    BufferedImage after = new BufferedImage(w2, h2, before.getType());
    AffineTransform scaleInstance = AffineTransform.getScaleInstance(scale, scale);
    AffineTransformOp scaleOp
            = new AffineTransformOp(scaleInstance, AffineTransformOp.TYPE_BILINEAR);

    Graphics2D g2 = (Graphics2D) after.getGraphics();
    // Here, you may draw anything you want into the new image, but we're just drawing
    // a scaled version of the original image. This is slower than 
    // calling scaleOp.filter().
    g2.drawImage(before, scaleOp, 0, 0);
    g2.dispose();
    return after;
}

/**
 * I call this one "questionable" because it uses the questionable getScaledImage() 
 * method. This method is no longer favored because it's slow, as my tests confirm.
 * @param before The original image
 * @param scale The scale factor
 * @return The scaled image.
 */
private static Image questionable(final BufferedImage before, double scale) {
    int w2 = (int) (before.getWidth() * scale);
    int h2 = (int) (before.getHeight() * scale);
    return before.getScaledInstance(w2, h2, Image.SCALE_FAST);
}
person MiguelMunoz    schedule 14.09.2017
comment
Можете ли вы также порекомендовать что-то, что дает плавные результаты при масштабировании вниз? getScaledInstance с Image.SCALE_SMOOTH работает, но, как всем известно, невероятно медленно. Все остальное, что я пробовал (включая AffineTransformOp и рисование с преобразованием, примененным с любой комбинацией RenderingHints), дает мне неровные края. - person user2543253; 05.12.2019
comment
Хорошо, я собираюсь предложить кое-что, но я понятия не имею, будет ли это работать хорошо или будет ли это быстрее. Попробуйте выполнить двухэтапное масштабирование, где первый этап представляет собой интегральную шкалу. Итак, если вам нужно масштабировать с коэффициентом 1/3,4, возьмите обратную величину (3,4) и усеките ее до целого числа. Это дает нам 3. Итак, уменьшите масштаб в 3 раза на первом этапе. Затем пройдите оставшуюся часть пути на втором этапе. (Это просто обоснованное предположение, но это первое, что я бы попробовал.) Вы также можете поискать какую-нибудь стороннюю библиотеку с хорошими методами масштабирования. (Некоторые были упомянуты на этой странице.) - person MiguelMunoz; 06.12.2019

Если вы не возражаете против использования внешней библиотеки, Thumbnailator может выполнить масштабирование BufferedImages.

Thumbnailator позаботится об обработке Java 2D (например, с помощью Graphics2D и установив соответствующий подсказки по рендерингу), чтобы можно было использовать простой свободный вызов API для изменения размера изображений:

BufferedImage image = Thumbnails.of(originalImage).scale(2.0).asBufferedImage();

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


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

person coobird    schedule 16.11.2014
comment
Это отличная библиотека! Миниатюры просто потрясающие по сравнению с Graphics2D - person Artem; 07.02.2016
comment
Отличная библиотека! Также хорошо сочетается с Kotlin. Также кажется более современным, чем некоторые другие варианты. - person ATutorMe; 01.10.2020

scale(..) работает немного по-другому. Вы можете использовать bufferedImage.getScaledInstance(..)

person Bozho    schedule 18.11.2010
comment
Я пробовал так, но getScaledInstance возвращает ToolkitImage, а для моей цели он мне не подходит. Благодарность - person Thiago Diniz; 19.11.2010
comment
вы можете преобразовать его в BufferedImage, скопировав его растр в новый BufferedImage. Поиск для «конвертировать изображение в буферизованное изображение» - person Bozho; 19.11.2010