Заставить BufferedImage использовать меньше оперативной памяти?

У меня есть java-программа, которая читает jpeg-файл с жесткого диска и использует его в качестве фонового изображения для других целей. Само изображение хранится в объекте BufferImage вот так:

BufferedImage background
background = ImageIO.read(file)

Это прекрасно работает — проблема в том, что сам объект BufferedImage огромен. Например, файл jpeg размером 215 КБ становится объектом BufferedImage размером 4 мегабайта и изменением. Рассматриваемое приложение может иметь загруженные довольно большие фоновые изображения, но в то время как jpeg никогда не превышает мегабайт или два, память, используемая для хранения BufferedImage, может быстро превысить 100 мегабайт.

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

Есть ли способ сохранить изображение в оперативной памяти в меньшем формате? Я нахожусь в ситуации, когда у меня больше слабины на стороне ЦП, чем на ОЗУ, поэтому небольшой удар по производительности, чтобы вернуть размер объекта изображения обратно к сжатию jpeg, стоил бы того.


person Electrons_Ahoy    schedule 20.07.2010    source источник


Ответы (6)


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

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

public static BufferedImage subsampleImage(
    ImageInputStream inputStream,
    int x,
    int y,
    IIOReadProgressListener progressListener) throws IOException {
    BufferedImage resampledImage = null;

    Iterator<ImageReader> readers = ImageIO.getImageReaders(inputStream);

    if(!readers.hasNext()) {
      throw new IOException("No reader available for supplied image stream.");
    }

    ImageReader reader = readers.next();

    ImageReadParam imageReaderParams = reader.getDefaultReadParam();
    reader.setInput(inputStream);

    Dimension d1 = new Dimension(reader.getWidth(0), reader.getHeight(0));
    Dimension d2 = new Dimension(x, y);
    int subsampling = (int)scaleSubsamplingMaintainAspectRatio(d1, d2);
    imageReaderParams.setSourceSubsampling(subsampling, subsampling, 0, 0);

    reader.addIIOReadProgressListener(progressListener);
    resampledImage = reader.read(0, imageReaderParams);
    reader.removeAllIIOReadProgressListeners();

    return resampledImage;
  }

 public static long scaleSubsamplingMaintainAspectRatio(Dimension d1, Dimension d2) {
    long subsampling = 1;

    if(d1.getWidth() > d2.getWidth()) {
      subsampling = Math.round(d1.getWidth() / d2.getWidth());
    } else if(d1.getHeight() > d2.getHeight()) {
      subsampling = Math.round(d1.getHeight() / d2.getHeight());
    }

    return subsampling;
  }

Чтобы получить ImageInputStream из файла, используйте:

ImageIO.createImageInputStream(new File("C:\\image.jpeg"));

Как видите, эта реализация также учитывает исходное соотношение сторон изображения. При желании вы можете зарегистрировать IIOReadProgressListener, чтобы отслеживать, какая часть изображения уже прочитана. Это полезно для отображения индикатора выполнения, например, если изображение читается по сети... Хотя это не обязательно, вы можете просто указать null.

Почему это имеет особое значение для вашей ситуации? Он никогда не считывает все изображение в память, ровно столько, сколько вам нужно, чтобы его можно было отобразить с желаемым разрешением. Очень хорошо работает с огромными изображениями, даже с 10 МБ на диске.

person S73417H    schedule 21.07.2010
comment
Это полезно и хорошее быстрое решение. Есть некоторые недостатки: это работает только тогда, когда изображение должно быть уменьшено в 2 раза или больше (из-за упрощенного алгоритма, используемого для выборки не более чем из каждого другого исходного столбца), и я также заметил некоторое ухудшение качества в небольшом % примера. картинки. - person Dan Gravell; 11.03.2013

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

Точно... Скажем, JPG 1920x1200 может поместиться, скажем, в 300 КБ, в то время как в памяти (обычно) RGB + альфа, 8 бит на компонент (следовательно, 32 бита на пиксель), которые он должен занимать в памяти:

1920 x 1200 x 32 / 8 = 9 216 000 bytes 

поэтому ваш файл размером 300 КБ становится изображением, требующим почти 9 МБ ОЗУ (обратите внимание, что в зависимости от типа изображений, которые вы используете с Java, и в зависимости от JVM и ОС иногда это может быть ОЗУ GFX-карты).

Если вы хотите использовать изображение в качестве фона рабочего стола 1920x1200, вам, вероятно, не нужно иметь изображение большего размера в памяти (если только вы не хотите использовать какой-то специальный эффект, такой как прореживание sub-rgb/сглаживание цвета/ и т.д.).

Итак, у вас есть выбор:

  1. делает ваши файлы менее широкими и менее высокими (в пикселях) на диске
  2. уменьшить размер изображения на лету

Я обычно использую номер 2, потому что уменьшение размера файла на жестком диске означает потерю деталей (изображение 1920x1200 менее детализировано, чем «то же самое» с разрешением 3940x2400: вы «теряете информацию» при уменьшении масштаба).

Теперь Java как бы отстойно манипулирует такими большими изображениями (как с точки зрения производительности, так и с точки зрения использования памяти и с точки зрения качества [*]). Раньше я вызывал ImageMagick из Java, чтобы сначала изменить размер изображения на диске, а затем загрузить изображение с измененным размером (скажем, подходящее к размеру моего экрана).

В настоящее время существуют Java-мосты/API для прямого взаимодействия с ImageMagick.

[*] Вы НИ В КОЕМ СЛУЧАЕ не можете уменьшить размер изображения с помощью встроенного API Java так же быстро и с таким же качеством, как у ImageMagick, для начала.

person SyntaxT3rr0r    schedule 20.07.2010

Вы должны использовать BufferedImage? Не могли бы вы написать свою собственную реализацию Image, которая хранит байты jpg в памяти и при необходимости конвертирует в BufferedImage, а затем отбрасывает?

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

person Stephen    schedule 21.07.2010

Используйте imgcalr.

http://www.thebuzzmedia.com/software/imgscalr-java-image-scaling-library/

Почему?

  1. Следует лучшим практикам
  2. Глупо просто
  3. Интерполяция, поддержка сглаживания
  4. Таким образом, вы не используете собственную библиотеку масштабирования.

Код:

BufferedImage thumbnail = Scalr.resize(image, 150);

or

BufferedImage thumbnail = Scalr.resize(image, Scalr.Method.SPEED, Scalr.Mode.FIT_TO_WIDTH, 150, 100, Scalr.OP_ANTIALIAS);

Кроме того, используйте image.flush() на своем увеличенном изображении после преобразования, чтобы помочь с использованием памяти.

person NoUserException    schedule 08.05.2013
comment
Разве Scalr не требует, чтобы исходное изображение загружалось как BufferedImage (там переменная image) полностью в первую очередь? На самом деле я был в процессе использования imgcalr, но мой оригинал слишком велик, чтобы начать. - person Kelly; 13.09.2014

Размер файла JPG на диске совершенно не имеет значения.
Размер файла в пикселях. Если ваше изображение имеет разрешение 15 мегапикселей, ожидайте, что для загрузки необработанной несжатой версии потребуется слишком много оперативной памяти.
Измените размеры изображения так, чтобы оно соответствовало вашим потребностям, и это лучшее, что вы можете сделать, не переходя на менее богатую версию. представление цветового пространства.

person Community    schedule 20.07.2010
comment
леденец: извините, но а) вы не отвечаете (отсюда -1) и b) ваш комментарий о том, что он неуместен СИЛЬНО вводит в заблуждение (жаль, что я не могу поставить -2). Проблема ОП, очевидно, заключается в том, что сжатие JPEG ОЧЕНЬ эффективно для некоторых изображений, и поэтому он удивлен, увидев разницу в размере сжатого JPEG / несжатого (A) RGB. Вы никак не помогаете. - person SyntaxT3rr0r; 21.07.2010
comment
Неважно, насколько велик исходный файл. Точно так же, как попытка прочитать заархивированный TXT-файл размером 1 ГБ не имеет значения для размера zip-файла. Размер файла на одном диске не имеет ничего общего с объемом памяти, который требуется для отображения несжатого изображения. - person ; 21.07.2010
comment
ответ есть, измените размер изображения на меньшие размеры - person ; 21.07.2010

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

BufferedImage background = new BufferedImage(
   width, 
   height, 
   BufferedImage.TYPE_INT_RGB
);

int[] pixels = background.getRaster().getPixels(
    0, 
    0, 
    imageBuffer.getWidth(), 
    imageBuffer.getHeight(), 
    (int[]) null
);
person karlphillip    schedule 20.07.2010
comment
отличный пример кода. Не могли бы вы добавить еще несколько разрывов, чтобы избежать прокрутки? - person Dinah; 21.07.2010