Сделать BufferedImage использовать меньше оперативной памяти? - PullRequest
13 голосов
/ 21 июля 2010

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

BufferedImage background
background = ImageIO.read(file)

Это прекрасно работает - проблема в том, что сам объект BufferedImage огромен.Например, файл в формате 215k jpeg становится объектом BufferedImag e, который составляет 4 мегабайта и изменяется.В рассматриваемом приложении могут быть загружены некоторые довольно большие фоновые изображения, но, хотя размер jpeg никогда не превышает одного или двух мегабайт, объем памяти, используемой для хранения BufferedImage, может быстро превысить 100 мегабайт.Все это потому, что изображение хранится в оперативной памяти как необработанные данные RGB, а не сжимается и не оптимизируется каким-либо образом.

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

Ответы [ 6 ]

10 голосов
/ 21 июля 2010

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

3 голосов
/ 21 июля 2010

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

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

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

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

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

Итак, вам нужно сделать выбор:

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

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

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

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

[*] Существует NO WAY Вы сокращаете изображение с помощью встроенного API Java так же быстро и с качеством, аналогичным тому, которое обеспечивает ImageMagick, для начала.

2 голосов
/ 09 мая 2013

Используйте imgscalr:

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

2 голосов
/ 21 июля 2010

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

1 голос
/ 21 июля 2010

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

0 голосов
/ 21 июля 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
);
...