Cargar fotografías grandes de forma eficiente

Las fotografías introducidas por el usuario pueden tener muy diversas procedencias, pudiendo llegar a los 16 Mpx en dispositivos de altas prestaciones. Cuando trabajas con fotografías es muy importante que tengas en cuenta que la memoria es un recurso limitado. Por lo tanto, es muy probable que cuando trates de cargar una imagen de gran tamaño, tu aplicación se detenga, mostrando en el LogCat el siguiente error:

Otro posible error cuando intentas cargar una imagen de ancho o alto mayor de 4096 px, es que la imagen no se muestre, aunque la aplicación continua ejecutándose. En este caso abre el LogCat y busca si aparece el siguiente warning:

Trabajar con una fotografía de muy alta definición, no tiene mucho sentido si la vamos a representar en una la pantalla de móvil, donde en raras ocasiones se supera 1 Mpx. Este problema se agrava cuando queremos mostrar una miniatura de la fotografía (thumbnail). ¿Para que cargar en memoria una fotografía en alta resolución, cuando solo la mostramos a baja resolución?

Una estrategia para evitar los problemas que hemos enumerado, puede consistir en cargar en memoria, no la fotografía original, sino una versión con una resolución adaptada a nuestras necesidades. La clase BitmapFactory nos ofrece herramientas para trabajar de forma adecuada[1]. Esta clase permite reescalar una imagen a una resolución menor, cargando en memoria un Bitmap de tamaño adecuado a nuestras necesidades. BitmapFactory solo admite factores de reducción que sean potencias de dos (1, 2, 4, 8, 16, etc.). Este factor de reducción se aplica al ancho y alto de la imagen. Por ejemplo, una imagen con resolución 2048x1536 con un factor de reducción 4, produce un Bitmap de aproximadamente 512x384. En el siguiente ejercicio se indica los pasos que tenemos que seguir para carga imágenes con una resolución adecuada.



[1] http://developer.android.com/training/displaying-bitmaps/load-bitmap.html

Ejercicio paso a paso: Evitando errores de memoria con fotografías grandes

1.     Trata de cargar imágenes de gran resolución en la aplicación Mis Lugares y observa si que producen alguno de los errores comentados.

2.     En el método ponerFoto()de la clase VistaLugar reemplaza:

imageView.setImageURI(Uri.parse(uri));

por:

imageView.setImageBitmap(reduceBitmap(this, uri, 1024,      1024));

Antes del cambio, poníamos la URI que nos indicaba el usuario con la foto, directamente en el ImageView. Esto suponía que  lo que se cargaba en memoria todos los pixeles de la imagen. En una fotografía de 16 Mpx, codificando cada pixel con 4 bytes, supone 64 Mb de memoria, lo que podría ocasionar graves problemas de memoria.

En el nuevo código propuesto, vamos a crear un  objeto Bitmap para asignarlo al ImageView. Este Bitmap es creado por el método reduceBitmap() a partir de la URI indicada. Pero ahora, reescalado a una resolución que no ha de superar al ancho y alto indicado. En Mis Lugares se ha decidido usar un ancho y alto inferior o igual a 1024. De esta forma, la imagen tendrá una resolución inferior a 1 Mpx y un consumo de memoria inferior a 4 Mb.  

3.     Añade el siguiente método a la clase VistaLugar:

public static Bitmap reduceBitmap(Context contexto, String uri,
                                          int maxAncho, int maxAlto) {
       try {
             final BitmapFactory.Options options = new BitmapFactory.Options();
             options.inJustDecodeBounds = true;
             BitmapFactory.decodeStream(contexto.getContentResolver()
                                  .openInputStream(Uri.parse(uri)), null, options);
             options.inSampleSize = (int) Math.max(
                                  Math.ceil(options.outWidth / maxAncho),
                                  Math.ceil(options.outHeight / maxAlto));
             options.inJustDecodeBounds = false;
             return BitmapFactory.decodeStream(contexto.getContentResolver()
                                 .openInputStream(Uri.parse(uri)), null, options);
       } catch (FileNotFoundException e) {
             Toast.makeText(contexto, "Fichero/recurso no encontrado",
                                  Toast.LENGTH_LONG).show();
             e.printStackTrace();
             return null;

       }
}

El propósito de este método es ontener un Bitmap a partir de la URI indicada pero reescalandolo a una resolución que no supere un ancho y un alto. El nuevo tamaño siempre se obtendrá dividiento ancho y alto por una potencia de dos. Es decir, el nuevo alto y ancho será la mitad, o una cuarta parte, o un octavo, … de las dimensiones originales.

La clase BitmapFactory nos ofrece varias alternativas para decodificar un Bitmapdesde distintas fuentes (decodeByteArray(), decodeFile(), decodeResource(), etc.). En Mis Lugares la fotografía puede provenir de un fichero, cuando se toma de la cámara o desde un ContentProvider cuando se selacciona de la galería. Para cubrir las procedencias consideramos que nos han parado un identificador de URI (Uri.parse(uri)). Así, podemos abrir el Stream asociado a la URI (openInputStream()) y este Stream puede ser decodificado utilizando el método  decodeStream().

En una primera decodificación no nos interesa cargar toda la fotografía en memoria, si no solo estamos interesados en obtener sus dimensiones originales. Para conseguir esto creamos un objeto de la clase BitmapFactory.Options y activamos la opción inJustDecodeBounds. De esta forma, la siguiente llamada a decodeStream() no decodificará toda la fotografía y la cargará en memoria, si no que se limitará obtener sus dimensiones y almacenarlas en options.outWidth y options.outHeight.

La siguiente línea calcula el factor de reducción deseado y lo almacena en inSampleSize. Para ello, se divide el ancho de la foto entre el ancho máximo redondeando hacia arriba. Se realiza la misma acción con el alto y se selecciona el mayor de los dos factores.

Acontinuación desactivamos la opción inJustDecodeBounds, dado que ahora si queremos que se decodifique la image, y hacemos la llamada a decodeStream(). En esta decodificación la opción inSampleSize actuará como factor de reducción.

Como hemos indicado, BitmapFactory solo trabaja con valores de reducción que coincidan con potencias de dos. Si se indica en inSampleSize un valor que no coincida con 1, 2, 4, 8, etc. se tomará la potencia de dos inferior. Un valor inferior a 1 se considerará como 1.

4.     Ejecuta de nuevo la aplicación y verifica que han desaparecido los problemas con la memoria. También es posible que con esta modificación las imágenes pierdan nitidez. Recuerda que ahora el ancho/alto mayor puede haberse reducido a un valor entre 512 y 1024. Si consideras que la resolución es insuficiente reemplaza los valores 1024 por algo mayor, como 2048.