images - load an image java
Haga que una imagen Buffered use menos RAM? (6)
Tengo el programa Java que lee un archivo jpeg del disco duro y lo usa como imagen de fondo para varias otras cosas. La imagen en sí misma se almacena en un objeto BufferImage
así:
BufferedImage background
background = ImageIO.read(file)
Esto funciona genial: el problema es que el objeto BufferedImage
sí es enorme. Por ejemplo, un archivo jpeg de 215k se convierte en un objeto BufferedImag
e que es de 4 megas y cambia. La aplicación en cuestión puede tener algunas imágenes de fondo bastante grandes cargadas, pero mientras que los jpegs nunca son más que un meg o dos, la memoria utilizada para almacenar BufferedImage
puede superar rápidamente los 100 s de megabytes.
Supongo que todo esto se debe a que la imagen se está almacenando en RAM como datos RGB sin procesar, no comprimidos u optimizados de ninguna manera.
¿Hay alguna manera de almacenar la imagen en RAM en un formato más pequeño? Estoy en una situación en la que tengo más holgura en el lado de la CPU que en la RAM, por lo que un pequeño golpe de rendimiento para recuperar el tamaño del objeto de imagen hacia la compresión de jpeg valdría la pena.
Supongo que todo esto se debe a que la imagen se está almacenando en RAM como datos RGB sin procesar, no comprimidos u optimizados de ninguna manera.
Exactamente ... Digamos que un JPG de 1920x1200 puede caber, digamos, 300 KB mientras está en la memoria, en un (típico) RGB + alpha, 8 bits por componente (por lo tanto, 32 bits por píxel) que ocupará, en la memoria:
1920 x 1200 x 32 / 8 = 9 216 000 bytes
entonces su archivo de 300 KB se convierte en una imagen que necesita casi 9 MB de RAM (tenga en cuenta que, dependiendo del tipo de imágenes que utilice desde Java y según la JVM y el sistema operativo, a veces puede ser RAM de la tarjeta GFX).
Si desea usar una imagen como fondo de una computadora de escritorio de 1920x1200, probablemente no necesite una imagen más grande que la de la memoria (a menos que desee algún efecto especial, como la eliminación de sub-rgb / anti-aliasing de color / etc.).
Entonces tienes que elegir:
- hace que sus archivos sean menos anchos y menos altos (en píxeles) en el disco
- reducir el tamaño de la imagen sobre la marcha
Normalmente voy con el número 2 porque reducir el tamaño del archivo en el disco duro significa que estás perdiendo detalles (una imagen de 1920x1200 es menos detallada que la "misma" en 3940x2400: estarías "perdiendo información" reduciéndola).
Ahora, Java es un poco apetecible a la hora de manipular imágenes tan grandes (desde el punto de vista del rendimiento, desde el punto de vista del uso de la memoria y desde el punto de vista de la calidad [*]). En los días en que yo llamaría a ImageMagick de Java para cambiar el tamaño de la imagen en el disco primero, y luego cargar la imagen redimensionada (por ejemplo, ajustar el tamaño de mi pantalla).
Hoy en día existen puentes / API de Java para interactuar directamente con ImageMagick.
[*] No hay forma de que estés reduciendo el tamaño de una imagen usando la API integrada de Java tan rápido y con una calidad tan buena como la proporcionada por ImageMagick, para empezar.
Podrías copiar los píxeles de la imagen a otro buffer y ver si eso ocupa menos memoria que el objeto BufferedImage. Probablemente algo como esto:
BufferedImage background= new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
int[] pixels = background.getRaster().getPixels(0, 0, imageBuffer.getWidth(), imageBuffer.getHeight(), (int[]) null);
¿Tienes que usar BufferedImage
? ¿Podría escribir su propia implementación de Image
que almacene los bytes jpg en la memoria, y se enmascara en una Imagen Buffered según sea necesario y luego descarte?
Esto se aplica con cierta lógica de visualización (reescalar la imagen usando JAI antes de almacenarla en su matriz de bytes como jpg), lo hará más rápido que decodificar el jpg grande cada vez, y una huella más pequeña que la que tiene actualmente (se exceptúan los requisitos de memoria de procesamiento) .
El tamaño del archivo JPG en el disco es completamente irrelevante .
Las dimensiones en píxeles del archivo son. Si su imagen es de 15 megapíxeles, se espera que requiera una gran cantidad de RAM para cargar una versión sin comprimir.
Cambie el tamaño de las dimensiones de su imagen para que sea exactamente lo que necesita y eso es lo mejor que puede hacer sin tener que recurrir a una representación del espacio de color menos rica.
Uno de mis proyectos es el de bajar la muestra de la imagen, ya que se está leyendo desde un ImageStream sobre la marcha. El muestreo descendente reduce las dimensiones de la imagen a un ancho y alto requeridos sin requerir costosos cálculos de cambio de tamaño o modificación de la imagen en el disco.
Como reduzco la muestra de la imagen a un tamaño más pequeño, también reduce significativamente la potencia de procesamiento y la RAM requeridas para mostrarla. Para una optimización adicional, también renderizo la imagen en búfer en mosaicos ... Pero eso está un poco fuera del alcance de esta discusión. Pruebe lo siguiente:
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;
}
Para obtener ImageInputStream desde un Archivo, use:
ImageIO.createImageInputStream(new File("C://image.jpeg"));
Como puede ver, esta implementación también respeta la relación de aspecto original de la imagen. Opcionalmente, puede registrar un IIOReadProgressListener para que pueda realizar un seguimiento de la cantidad de imágenes que se han leído hasta el momento. Esto es útil para mostrar una barra de progreso si la imagen se lee en una red, por ejemplo ... No es necesario, solo puede especificar nulo.
¿Por qué esto es de particular relevancia para su situación? Nunca lee la imagen completa en la memoria, tanto como lo necesita para que se pueda mostrar con la resolución deseada. Funciona muy bien para imágenes enormes, incluso aquellas que son 10 de MB en disco.
Use imgscalr:
http://www.thebuzzmedia.com/software/imgscalr-java-image-scaling-library/
¿Por qué?
- Sigue las mejores prácticas
- Estúpido simple
- Soporte de interpolación, anti-aliasing
- Así que no estás rodando tu propia biblioteca de escalado
Código:
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);
Además, use image.flush()
en su imagen más grande después de la conversión para ayudar con la utilización de la memoria.