Направете BufferedImage да използва по-малко RAM?

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

BufferedImage background
background = ImageIO.read(file)

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

Предполагам, че всичко това е така, защото изображението се съхранява в ram като необработени RGB данни, не е компресирано или оптимизирано по никакъв начин.

Има ли начин да съхранява изображението в ram в по-малък формат? Намирам се в ситуация, в която имам повече хлабина от страна на процесора, отколкото от RAM, така че лек удар в производителността, за да върна размера на обекта на изображението обратно към jpeg компресията, би си заслужавал.


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


Отговори (6)


В един от моите проекти просто намалявам образеца на изображението, докато се чете от ImageStream в движение. Намаляването на семплирането намалява размерите на изображението до необходимата ширина и височина, като същевременно не изисква скъпи изчисления за преоразмеряване или модификация на изображението на диска.

Тъй като намалявам изображението до по-малък размер, това също значително намалява мощността на обработка и RAM, необходими за показването му. За допълнителна оптимизация изобразявам буферираното изображение и в плочки... Но това е малко извън обхвата на тази дискусия. Опитайте следното:

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 MB на диск.

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

Предполагам, че всичко това е така, защото изображението се съхранява в ram като необработени RGB данни, не е компресирано или оптимизирано по никакъв начин.

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

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

така че вашият файл от 300 KB се превръща в картина, която се нуждае от почти 9 MB RAM (имайте предвид, че в зависимост от типа изображения, които използвате от Java и в зависимост от JVM и OS, това понякога може да е RAM на 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

Използвайте 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() на вашето по-голямо изображение след конвертирането, за да помогнете с използването на паметта.

person NoUserException    schedule 08.05.2013
comment
Scalr не изисква ли първо оригиналното изображение да бъде заредено като BufferedImage (променливата image там)? Всъщност бях в средата на използването на imgscalr, но оригиналът ми е твърде голям, за да започна. - person Kelly; 13.09.2014

Размерът на JPG файла на диска е напълно без значение.
Размерите на файла в пиксели са. Ако изображението ви е 15 мегапиксела, очаквайте, че ще изисква голямо натоварване от RAM, за да зареди необработена некомпресирана версия.
Преоразмерете размерите на изображението си, за да бъдат точно това, от което се нуждаете, и това е най-доброто, което можете да направите, без да отивате към по-малко богата представяне на цветовото пространство.

person Community    schedule 20.07.2010
comment
близалка: Съжалявам, но a) не отговаряте (следователно -1) и b) вашият коментар, че е без значение е ГОЛЯМО подвеждащо (жалко, че не мога да дам -2). Проблемът на OP очевидно е, че компресията на JPEG е МНОГО ефективна за някакъв вид изображение и следователно той е изненадан да види разликата в размера на компресиран JPEG / некомпресиран (A)RGB. Не помагаш по никакъв начин. - person SyntaxT3rr0r; 21.07.2010
comment
Няма значение колко голям е изходният файл. Точно както при опит за четене на 1GB TXT файл, който е компресиран, размерът на 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