¿Cómo hacer un downsample de imágenes correctamente?

Fondo

Creación de una aplicación que tiene un montón de imágenes de alta calidad, he decidido reducir la escala de las imágenes al tamaño necesario (lo que significa que si la imagen es más grande que la pantalla, yo downscale).

El problema

He notado que en algunos dispositivos, si las imágenes se reducen, se vuelven borrosas / pixeladas, pero en los mismos dispositivos, para el mismo tamaño de imagen de destino, si las imágenes no están reducidas, se ven bien.

Lo que he intentado

He decidido revisar esta cuestión más y creé una pequeña aplicación POC que muestra el problema.

Antes de mostrar el código, he aquí una demostración de lo que estoy hablando:

Introduzca aquí la descripción de la imagen

Es un poco difícil ver la diferencia, pero se puede ver que el segundo es un poco pixelado. Esto se puede mostrar en cualquier imagen.

public class MainActivity extends Activity { @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); final ImageView originalImageView=(ImageView)findViewById(R.id.originalImageView); final ImageView halvedImageView=(ImageView)findViewById(R.id.halvedImageView); final ImageView halvedBitmapImageView=(ImageView)findViewById(R.id.halvedBitmapImageView); // final Bitmap originalBitmap=BitmapFactory.decodeResource(getResources(),R.drawable.test); originalImageView.setImageBitmap(originalBitmap); halvedImageView.setImageBitmap(originalBitmap); // final LayoutParams layoutParams=halvedImageView.getLayoutParams(); layoutParams.width=originalBitmap.getWidth()/2; layoutParams.height=originalBitmap.getHeight()/2; halvedImageView.setLayoutParams(layoutParams); // final Options options=new Options(); options.inSampleSize=2; // options.inDither=true; //didn't help // options.inPreferQualityOverSpeed=true; //didn't help final Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.test,options); halvedBitmapImageView.setImageBitmap(bitmap); } } 

Xml:

 <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" android:fillViewport="true"> <HorizontalScrollView android:layout_width="match_parent" android:fillViewport="true" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="original" /> <ImageView android:layout_width="wrap_content" android:id="@+id/originalImageView" android:layout_height="wrap_content" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="original , imageView size is halved" /> <ImageView android:layout_width="wrap_content" android:id="@+id/halvedImageView" android:layout_height="wrap_content" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="bitmap size is halved" /> <ImageView android:layout_width="wrap_content" android:id="@+id/halvedBitmapImageView" android:layout_height="wrap_content" /> </LinearLayout> </HorizontalScrollView> </ScrollView> 

La pregunta

¿Por qué ocurre?

Ambos métodos deben tener el mismo resultado, ya que tanto la muestra de la misma fuente y utilizando el mismo factor.

He intentado jugar con el método de downsampling, pero nada ha ayudado.

El uso de inDensity (en lugar de inSampleSize) parece arreglarlo, pero no estoy seguro de qué establecerlo. Pienso que para las imágenes exteriores (del Internet por ejemplo), puedo fijarla a la densidad de la pantalla multiplicada por el tamaño de la muestra que deseo utilizar.

¿Pero es incluso una buena solución? ¿Qué debo hacer en el caso de las imágenes están dentro de la carpeta de recursos (no creo que haya una función para obtener la carpeta de densidad de un mapa de bits se encuentra en)? ¿Por qué funciona mientras se utiliza la forma recomendada (se habla aquí ) no funciona bien?


EDIT: He encontrado un truco para conseguir que la densidad se utiliza para un dibujable se obtiene de los recursos ( enlace aquí ). Sin embargo, no es una prueba futura, ya que necesita ser específico de la densidad para detectar.

Ok, he encontrado una buena alternativa, que creo que debería funcionar para cualquier tipo de decodificación de mapa de bits.

No sólo eso, sino que también le permite reducir la escala utilizando cualquier tamaño de muestra que desee, y no sólo el poder de 2. Si usted pone más esfuerzo a él, usted puede incluso utilizar fracciones en vez de enteros para el downscaling.

El código siguiente funciona para las imágenes de la carpeta res, pero se puede hacer fácilmente para cualquier tipo de decodificación de mapa de bits:

 private Bitmap downscaleBitmapUsingDensities(final int sampleSize,final int imageResId) { final Options bitmapOptions=new Options(); bitmapOptions.inDensity=sampleSize; bitmapOptions.inTargetDensity=1; final Bitmap scaledBitmap=BitmapFactory.decodeResource(getResources(),imageResId,bitmapOptions); scaledBitmap.setDensity(Bitmap.DENSITY_NONE); return scaledBitmap; } 

Lo he probado y muestra las imágenes downsampled muy bien. En la imagen de abajo, he mostrado la imagen original, y downscaling la imagen utilizando el método inSampleSize, y utilizando mi método.

Es difícil ver la diferencia, pero el que utiliza la densidad en realidad no sólo omite píxeles, pero utiliza todos ellos para tener en cuenta. Puede ser un poco más lento, pero es más preciso y utiliza una mejor interpolación.

Introduzca aquí la descripción de la imagen

La única desventaja en comparación con el uso de inSampleSize parece ser la velocidad, que es mejor en inSampleSize porque inSampleSize omite píxeles y porque el método de densidades hace cálculos adicionales en los píxeles omitidos.

Sin embargo, creo que de alguna manera android ejecuta ambos métodos en aproximadamente la misma velocidad.

Creo que la comparación de los 2 métodos es similar a la comparación entre el muestreo descendente del vecino más cercano y el muestreo descendente de la interpolación bilineal .

EDIT: he encontrado una desventaja del método que he mostrado aquí, en comparación con el que tiene Google. La memoria utilizada durante el proceso puede ser bastante alta, y creo que depende de la imagen en sí. Esto significa que usted debe usarlo sólo en los casos que usted piensa que tienen sentido.


EDIT: He hecho una solución combinada (tanto la solución de google y la mía) para aquellos que desean superar el problema de memoria. No es perfecto, pero es mejor que lo que hice antes, porque no usará tanta memoria como el bitmap original necesita durante el downsampling. En su lugar, utilizará la memoria como se utiliza en la solución de google.

Aquí está el código:

  // as much as possible, use google's way to downsample: bitmapOptions.inSampleSize = 1; bitmapOptions.inDensity = 1; bitmapOptions.inTargetDensity = 1; while (bitmapOptions.inSampleSize * 2 <= inSampleSize) bitmapOptions.inSampleSize *= 2; // if google's way to downsample isn't enough, do some more : if (bitmapOptions.inSampleSize != inSampleSize) { // downsample by bitmapOptions.inSampleSize/originalSampleSize . bitmapOptions.inTargetDensity = bitmapOptions.inSampleSize; bitmapOptions.inDensity = inSampleSize; } else if(sampleSize==1) { bitmapOptions.inTargetDensity=preferHeight ? reqHeight : reqWidth; bitmapOptions.inDensity=preferHeight ? height : width; } 

Así, en resumen, los pros y los contras de ambos métodos:

El modo de Google (usando inSampleSize) utiliza menos memoria durante la decodificación y es más rápido. Sin embargo, causa algunos artefactos gráficos a veces y sólo soporta downsampling a la potencia de 2, por lo que el bitmap de resultados podría tomar más de lo que quería (por ejemplo tamaño de x1 / 4 en lugar de x1 / 7).

Mi forma (usando densidades) es más precisa, da imágenes de mayor calidad y utiliza menos memoria en el mapa de bits de resultados. Sin embargo, puede utilizar una gran cantidad de memoria durante la decodificación (depende de la entrada) y es un poco más lento.


EDIT: otra mejora, ya que he descubierto que en algunos casos la imagen de salida no coincide con la restricción de tamaño requerida, y no desea bajar la muestra demasiado usando el modo de Google:

  final int newWidth = width / bitmapOptions.inSampleSize, newHeight = height / bitmapOptions.inSampleSize; if (newWidth > reqWidth || newHeight > reqHeight) { if (newWidth * reqHeight > newHeight * reqWidth) { // prefer width, as the width ratio is larger bitmapOptions.inTargetDensity = reqWidth; bitmapOptions.inDensity = newWidth; } else { // prefer height bitmapOptions.inTargetDensity = reqHeight; bitmapOptions.inDensity = newHeight; } } 

Así, por ejemplo, el downsampling desde la imagen 2448×3264 a 1200×1200, se convertirá en 900×1200

Debe utilizar inSampleSize. Para calcular el tamaño de la muestra que debe usar, haga lo siguiente.

 BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; Bitmap map = BitmapFactory.decodeFile(file.getAbsolutePath(), options); int originalHeight = options.outHeight; int originalWidth = options.outWidth; // Calculate your sampleSize based on the requiredWidth and originalWidth // For eg you want the width to stay consistent at 500dp int requiredWidth = 500 * getResources().getDisplayMetrics().density; int sampleSize = originalWidth / requiredWidth; // If the original image is smaller than required, don't sample if(sampleSize < 1) { sampleSize = 1; } options.inSampleSize = sampleSize; options.inPurgeable = true; options.inPreferredConfig = Bitmap.Config.RGB_565; options.inJustDecodeBounds = false; Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options); 

Espero que esto ayude.

Para mí, sólo la reducción de escala utilizando inSampleSize se comportó bien (pero no como el algoritmo del vecino más cercano). Pero desafortunadamente, esto no permite obtener la resolución exacta que necesitamos (exactamente exactamente enteros veces más pequeño que el original).

Así que encontré que la solución de SonyMobile de este problema funciona mejor para tal tarea.

Brevemente, consta de 2 pasos:

  1. Downscale usando BitmapFactory.Options :: inSampleSize-> BitmapFactory.decodeResource () tan cerca como sea posible de la resolución que usted necesita, pero no menos de lo que
  2. Llegar a la resolución exacta mediante la reducción de escala un poco utilizando Canvas :: drawBitmap ()

Aquí está una explicación detallada de cómo SonyMobile resolvió esta tarea: http://developer.sonymobile.com/2011/06/27/how-to-scale-images-for-your-android-application/

Aquí está el código fuente de las utilidades de escala de SonyMobile: http://developer.sonymobile.com/downloads/code-example-module/image-scaling-code-example-for-android/

  • Java: escala Mapsforge Map al usar mosaicos de mapa de bits en línea en lugar del renderizador sin conexión
  • DecodeByteArray y copyPixelsToBuffer no funcionan. SkImageDecoder :: Factory devuelto null
  • Android, guardar la imagen de la vista como archivo png
  • Android: desliza la imagen a través de "anulación" y "fitCenter" deforma Bitmap
  • Android: Cambiar el tamaño de un archivo de mapa de bits grande a un archivo de salida escalado
  • Mapbox GL utilizando mapas externos
  • Android: cómo convertir matriz de bytes a mapa de bits?
  • Pasar Bitmap a otra actividad termina en RunTimeException
  • Cargar correctamente datos de mapa de bits de 24 bits en un objeto Bitmap de 32 bits
  • Elige la imagen de la tarjeta sd, cambia el tamaño de la imagen y la guarda de nuevo en la tarjeta sd
  • Convertir elementos de vista de lista en una sola imagen de mapa de bits
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.