2010-11-20 24 views
26

Tengo un archivo de imagen en el disco y estoy redimensionando el archivo y guardándolo de nuevo en el disco como un nuevo archivo de imagen. Por el bien de esta pregunta, no los traigo a la memoria para mostrarlos en la pantalla, solo para cambiarles el tamaño y volver a guardarlos. Todo esto funciona bien. Sin embargo, las imágenes escaladas tienen artefactos como se muestra aquí: android: quality of the images resized in runtimeProblemas de calidad al cambiar el tamaño de una imagen en tiempo de ejecución

Se guardan con esta distorsión, ya que puedo sacarlos del disco y mirarlos en mi computadora y todavía tienen el mismo problema.

estoy usando un código similar a este Strange out of memory issue while loading an image to a Bitmap object para decodificar el mapa de bits en la memoria:

BitmapFactory.Options options = new BitmapFactory.Options(); 
options.inJustDecodeBounds = true; 
BitmapFactory.decodeFile(imageFilePathString, options); 

int srcWidth = options.outWidth; 
int srcHeight = options.outHeight; 
int scale = 1; 

while(srcWidth/2 > desiredWidth){ 
    srcWidth /= 2; 
    srcHeight /= 2; 
    scale *= 2; 
} 

options.inJustDecodeBounds = false; 
options.inDither = false; 
options.inSampleSize = scale; 
Bitmap sampledSrcBitmap = BitmapFactory.decodeFile(imageFilePathString, options); 

Entonces estoy haciendo la escala real con:

Bitmap scaledBitmap = Bitmap.createScaledBitmap(sampledSrcBitmap, desiredWidth, desiredHeight, false); 

Por último, la nueva imagen de tamaño se guardará en el disco con:

FileOutputStream out = new FileOutputStream(newFilePathString); 
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out); 

Luego, como mencioné, si saco th en el archivo del disco y mire, tiene ese problema de calidad vinculado anteriormente y se ve terrible. Si omito el createScaledBitmap y simplemente guardo el SampledSrcBitmap nuevamente en el disco, no hay problema, parece suceder solo si el tamaño cambia.

He tratado, como se puede ver en el código, el establecimiento de inDither en false como se ha mencionado aquí http://groups.google.com/group/android-developers/browse_thread/thread/8b1abdbe881f9f71 y como se mencionó en el post primera vinculado anteriormente. Eso no cambió nada. Además, en el primer post he vinculado, Romain tipo dijo:

En lugar de cambiar el tamaño a la hora (que va a ser muy costoso) dibujo, intenta cambiar el tamaño de un mapa de bits fuera de pantalla y asegúrese de que Bitmap es 32 bits (ARGB888).

Sin embargo, no tengo idea de cómo asegurarme de que Bitmap se mantenga en 32 bits durante todo el proceso.

También he leído un par de otros artículos como este http://android.nakatome.net/2010/04/bitmap-basics.html pero todos parecían dirigirse a dibujar y mostrar el mapa de bits, solo quiero cambiar el tamaño y guardarlo en el disco sin este problema de calidad.

Gracias mucho

+0

¿Está cambiando el tamaño de las imágenes para resolver problemas con varias densidades de pantalla? Si es así, supongo que ejecutará este código en el arranque inicial y no en el inicio posterior ... ¿O lo está ejecutando localmente para obtener nuevos activos (es decir, imágenes) para pantallas pequeñas? Solo tengo curiosidad porque me estoy encontrando con errores en pantallas de densidad pequeña. – gary

+0

En mi caso, no se trata realmente de la densidad de la pantalla. Solo estoy intentando colocar un conjunto de imágenes en un contenedor determinado. – cottonBallPaws

Respuesta

53

Después de experimentar por fin he encontrado una manera de hacer esto con resultados de buena calidad. Escribiré esto para cualquier persona que pueda encontrar útil esta respuesta en el futuro.

Para resolver el primer problema, los artefactos y el extraño tramado introducido en las imágenes, necesita asegurar que su imagen permanezca como una imagen ARGB_8888 de 32 bits. Usando el código en mi pregunta, simplemente puede agregar esta línea a las opciones antes de la segunda decodificación.

options.inPreferredConfig = Bitmap.Config.ARGB_8888; 

Después de agregar eso, los artefactos habían desaparecido pero los bordes de las imágenes se veían dentados en lugar de crujientes. Después de experimentar un poco más descubrí que cambiar el tamaño del mapa de bits usando una matriz en lugar de Bitmap.createScaledBitmap producía resultados mucho más nítidos.

Con estas dos soluciones, las imágenes ahora están cambiando de tamaño perfectamente.A continuación se muestra el código que estoy usando en caso de que beneficie a alguien más que se encuentre con este problema.

// Get the source image's dimensions 
BitmapFactory.Options options = new BitmapFactory.Options(); 
options.inJustDecodeBounds = true; 
BitmapFactory.decodeFile(STRING_PATH_TO_FILE, options); 

int srcWidth = options.outWidth; 
int srcHeight = options.outHeight; 

// Only scale if the source is big enough. This code is just trying to fit a image into a certain width. 
if(desiredWidth > srcWidth) 
    desiredWidth = srcWidth; 



// Calculate the correct inSampleSize/scale value. This helps reduce memory use. It should be a power of 2 
// from: https://stackoverflow.com/questions/477572/android-strange-out-of-memory-issue/823966#823966 
int inSampleSize = 1; 
while(srcWidth/2 > desiredWidth){ 
    srcWidth /= 2; 
    srcHeight /= 2; 
    inSampleSize *= 2; 
} 

float desiredScale = (float) desiredWidth/srcWidth; 

// Decode with inSampleSize 
options.inJustDecodeBounds = false; 
options.inDither = false; 
options.inSampleSize = inSampleSize; 
options.inScaled = false; 
options.inPreferredConfig = Bitmap.Config.ARGB_8888; 
Bitmap sampledSrcBitmap = BitmapFactory.decodeFile(STRING_PATH_TO_FILE, options); 

// Resize 
Matrix matrix = new Matrix(); 
matrix.postScale(desiredScale, desiredScale); 
Bitmap scaledBitmap = Bitmap.createBitmap(sampledSrcBitmap, 0, 0, sampledSrcBitmap.getWidth(), sampledSrcBitmap.getHeight(), matrix, true); 
sampledSrcBitmap = null; 

// Save 
FileOutputStream out = new FileOutputStream(NEW_FILE_PATH); 
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out); 
scaledBitmap = null; 

EDIT: Después del trabajo continuo en esto he encontrado que las imágenes todavía no son 100% perfectas. Haré una actualización si puedo mejorarlo.

Actualización: Después de revistir esto, encontré this question on SO y hubo una respuesta que mencionó la opción inScaled. Esto también ayudó con la calidad, así que agregué la respuesta actualizada para incluirla. También ahora anulo los bitmaps después de que terminan de ser utilizados.

Además, como nota al margen, si está utilizando estas imágenes en una vista Web, asegúrese de tomar this post into consideration.

Nota: también se debe añadir una comprobación para asegurarse de que la anchura y la altura son números válidos (no -1). Si lo son, provocará que el ciclo inSampleSize se vuelva infinito.

+0

marque esta respuesta si quiere calidad ARGB_8888: http://stackoverflow.com/questions/3440690/rotating-a-bitmap-using-matrix –

+0

Esto funcionó para mí, ¡gracias! Mucho mejores resultados en miniatura. Es posible que desee eliminar su variable srcHeight, ya que no se utiliza. –

+0

Esto funcionó muy bien, pero el único problema es cuando la imagen que se va a cambiar de tamaño está en posición vertical, después de cambiar el tamaño se convierte en ajardinado. ¿Alguna idea de cómo prevenir eso? –

8

En mi situación, estoy dibujando la imagen en la pantalla. Esto es lo que hice para que mis imágenes se vean correctas (una combinación de la respuesta de LittleFluffyKitty, además de algunas otras).

Para mis opciones cuando realmente cargar la imagen (utilizando decodeResource) configurar los siguientes valores:

options.inScaled = false; 
    options.inDither = false; 
    options.inPreferredConfig = Bitmap.Config.ARGB_8888; 

cuando realmente dibujar la imagen, creé mi objeto pintura como esto:

Paint paint = new Paint(); 
    paint.setAntiAlias(true); 
    paint.setFilterBitmap(true); 
    paint.setDither(true); 

Esperemos que alguien más también lo encuentre útil. Ojalá hubiera solo opciones para "Sí, deje que mis imágenes redimensionadas parezcan basura" y "No, no obligue a mis usuarios a sacar sus cucharillas con los ojos" en lugar de a la gran cantidad de opciones diferentes. Sé que quieren darnos mucho control, pero tal vez algunos métodos de ayuda para configuraciones comunes podrían ser útiles.

+0

Funciona como un amuleto. Gracias :) Salvaste mi día –

2

"Sin embargo, no tengo idea de cómo asegurarme de que Bitmap se mantenga como 32 bits durante todo el proceso."

Quería publicar una solución alternativa, que se ocupa de mantener la configuración ARGB_8888 intacta. NOTA: Este código solo decodifica bitmaps y necesita ser extendido, por lo que podría almacenar un mapa de bits.

Asumo que está escribiendo código para una versión de Android inferior a 3,2 (nivel API < 12), ya que desde entonces el comportamiento de los métodos

BitmapFactory.decodeFile(pathToImage); 
BitmapFactory.decodeFile(pathToImage, opt); 
bitmapObject.createScaledBitmap(bitmap, desiredWidth, desiredHeight, false /*filter?*/); 

ha cambiado.

En las plataformas anteriores (nivel API < 12) los métodos BitmapFactory.decodeFile (..) intentan devolver un mapa de bits con configuración RGB_565 por defecto, si no pueden encontrar ningún alfa, lo que reduce la calidad de una iamge. Esto es todavía bien, porque se puede hacer cumplir un mapa de bits utilizando ARGB_8888

options.inPrefferedConfig = Bitmap.Config.ARGB_8888 
options.inDither = false 

El verdadero problema viene cuando cada píxel de la imagen tiene un valor alfa de 255 (es decir, totalmente opaco). En ese caso, el indicador de mapa de bits 'hasAlpha' se establece en falso, aunque su Bitmap tenga la configuración ARGB_8888. Si su * .png-file tenía al menos un píxel real transparente, esta bandera se habría establecido en verdadero y no tendría que preocuparse por nada.

Por eso, cuando se desea crear un mapa de bits a escala utilizando

bitmapObject.createScaledBitmap(bitmap, desiredWidth, desiredHeight, false /*filter?*/); 

el método comprueba si la bandera 'hasAlpha' se establece en verdadero o falso, y en su caso se establece en false, lo que resulta en obtener un mapa de bits escalado, que se convirtió automáticamente al formato RGB_565.

tanto en el nivel API> = 12 hay un método público denominado

public void setHasAlpha (boolean hasAlpha); 

que habría resuelto este problema. Hasta ahora, esto fue solo una explicación del problema. Hice algunas investigaciones y noté que el método setHasAlpha existe desde hace mucho tiempo y es público, pero ha estado oculto (@hide annotation). Así es como se define en Android 2.3:

/** 
* Tell the bitmap if all of the pixels are known to be opaque (false) 
* or if some of the pixels may contain non-opaque alpha values (true). 
* Note, for some configs (e.g. RGB_565) this call is ignore, since it does 
* not support per-pixel alpha values. 
* 
* This is meant as a drawing hint, as in some cases a bitmap that is known 
* to be opaque can take a faster drawing case than one that may have 
* non-opaque per-pixel alpha values. 
* 
* @hide 
*/ 
public void setHasAlpha(boolean hasAlpha) { 
    nativeSetHasAlpha(mNativeBitmap, hasAlpha); 
} 

Ahora aquí es mi propuesta de solución. No se trata de cualquier copia de los datos de mapa de bits:

  1. controlados en tiempo de ejecución usando java.lang.reflect si la corriente aplicación de mapa de bits tiene un método público 'setHasAplha'. (Según mis pruebas, funciona perfectamente desde API nivel 3, y no he probado versiones inferiores, porque JNI no funcionaría). Puede tener problemas si un fabricante explícitamente lo ha hecho privado, protegido o eliminado.

  2. Llamar al método 'setHasAlpha' para un objeto de mapa de bits dado utilizando JNI. Esto funciona perfectamente, incluso para métodos o campos privados. Es oficial que JNI no verifica si está violando las reglas de control de acceso o no. Fuente: http://java.sun.com/docs/books/jni/html/pitfalls.html (10.9) Esto nos da una gran potencia, que debe usarse con prudencia. No trataría de modificar un campo final, incluso si funcionaría (solo para dar un ejemplo). Y por favor, tenga en cuenta que esto es sólo una solución ...

Aquí es mi aplicación de todos los métodos necesarios:

JAVA PARTE:

// NOTE: this cannot be used in switch statements 
    private static final boolean SETHASALPHA_EXISTS = setHasAlphaExists(); 

    private static boolean setHasAlphaExists() { 
     // get all puplic Methods of the class Bitmap 
     java.lang.reflect.Method[] methods = Bitmap.class.getMethods(); 
     // search for a method called 'setHasAlpha' 
     for(int i=0; i<methods.length; i++) { 
      if(methods[i].getName().contains("setHasAlpha")) { 
       Log.i(TAG, "method setHasAlpha was found"); 
       return true; 
      } 
     } 
     Log.i(TAG, "couldn't find method setHasAlpha"); 
     return false; 
    } 

    private static void setHasAlpha(Bitmap bitmap, boolean value) { 
     if(bitmap.hasAlpha() == value) { 
      Log.i(TAG, "bitmap.hasAlpha() == value -> do nothing"); 
      return; 
     } 

     if(!SETHASALPHA_EXISTS) { // if we can't find it then API level MUST be lower than 12 
      // couldn't find the setHasAlpha-method 
      // <-- provide alternative here... 
      return; 
     } 

     // using android.os.Build.VERSION.SDK to support API level 3 and above 
     // use android.os.Build.VERSION.SDK_INT to support API level 4 and above 
     if(Integer.valueOf(android.os.Build.VERSION.SDK) <= 11) { 
      Log.i(TAG, "BEFORE: bitmap.hasAlpha() == " + bitmap.hasAlpha()); 
      Log.i(TAG, "trying to set hasAplha to true"); 
      int result = setHasAlphaNative(bitmap, value); 
      Log.i(TAG, "AFTER: bitmap.hasAlpha() == " + bitmap.hasAlpha()); 

      if(result == -1) { 
       Log.e(TAG, "Unable to access bitmap."); // usually due to a bug in the own code 
       return; 
      } 
     } else { //API level >= 12 
      bitmap.setHasAlpha(true); 
     } 
    } 

    /** 
    * Decodes a Bitmap from the SD card 
    * and scales it if necessary 
    */ 
    public Bitmap decodeBitmapFromFile(String pathToImage, int pixels_limit) { 
     Bitmap bitmap; 

     Options opt = new Options(); 
     opt.inDither = false; //important 
     opt.inPreferredConfig = Bitmap.Config.ARGB_8888; 
     bitmap = BitmapFactory.decodeFile(pathToImage, opt); 

     if(bitmap == null) { 
      Log.e(TAG, "unable to decode bitmap"); 
      return null; 
     } 

     setHasAlpha(bitmap, true); // if necessary 

     int numOfPixels = bitmap.getWidth() * bitmap.getHeight(); 

     if(numOfPixels > pixels_limit) { //image needs to be scaled down 
      // ensures that the scaled image uses the maximum of the pixel_limit while keeping the original aspect ratio 
      // i use: private static final int pixels_limit = 1280*960; //1,3 Megapixel 
      imageScaleFactor = Math.sqrt((double) pixels_limit/(double) numOfPixels); 
      Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, 
        (int) (imageScaleFactor * bitmap.getWidth()), (int) (imageScaleFactor * bitmap.getHeight()), false); 

      bitmap.recycle(); 
      bitmap = scaledBitmap; 

      Log.i(TAG, "scaled bitmap config: " + bitmap.getConfig().toString()); 
      Log.i(TAG, "pixels_limit = " + pixels_limit); 
      Log.i(TAG, "scaled_numOfpixels = " + scaledBitmap.getWidth()*scaledBitmap.getHeight()); 

      setHasAlpha(bitmap, true); // if necessary 
     } 

     return bitmap; 
    } 

Carga tu lib y declarar el método nativo:

static { 
    System.loadLibrary("bitmaputils"); 
} 

private static native int setHasAlphaNative(Bitmap bitmap, boolean value); 

sección nativo (carpeta 'JNI')

Android.mk:

LOCAL_PATH := $(call my-dir) 

include $(CLEAR_VARS) 
LOCAL_MODULE := bitmaputils 
LOCAL_SRC_FILES := bitmap_utils.c 
LOCAL_LDLIBS := -llog -ljnigraphics -lz -ldl -lgcc 
include $(BUILD_SHARED_LIBRARY) 

bitmapUtils.c:

#include <jni.h> 
#include <android/bitmap.h> 
#include <android/log.h> 

#define LOG_TAG "BitmapTest" 
#define Log_i(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__) 
#define Log_e(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__) 


// caching class and method IDs for a faster subsequent access 
static jclass bitmap_class = 0; 
static jmethodID setHasAlphaMethodID = 0; 

jint Java_com_example_bitmaptest_MainActivity_setHasAlphaNative(JNIEnv * env, jclass clazz, jobject bitmap, jboolean value) { 
    AndroidBitmapInfo info; 
    void* pixels; 


    if (AndroidBitmap_getInfo(env, bitmap, &info) < 0) { 
     Log_e("Failed to get Bitmap info"); 
     return -1; 
    } 

    if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) { 
     Log_e("Incompatible Bitmap format"); 
     return -1; 
    } 

    if (AndroidBitmap_lockPixels(env, bitmap, &pixels) < 0) { 
     Log_e("Failed to lock the pixels of the Bitmap"); 
     return -1; 
    } 


    // get class 
    if(bitmap_class == NULL) { //initializing jclass 
     // NOTE: The class Bitmap exists since API level 1, so it just must be found. 
     bitmap_class = (*env)->GetObjectClass(env, bitmap); 
     if(bitmap_class == NULL) { 
      Log_e("bitmap_class == NULL"); 
      return -2; 
     } 
    } 

    // get methodID 
    if(setHasAlphaMethodID == NULL) { //initializing jmethodID 
     // NOTE: If this fails, because the method could not be found the App will crash. 
     // But we only call this part of the code if the method was found using java.lang.Reflect 
     setHasAlphaMethodID = (*env)->GetMethodID(env, bitmap_class, "setHasAlpha", "(Z)V"); 
     if(setHasAlphaMethodID == NULL) { 
      Log_e("methodID == NULL"); 
      return -2; 
     } 
    } 

    // call java instance method 
    (*env)->CallVoidMethod(env, bitmap, setHasAlphaMethodID, value); 

    // if an exception was thrown we could handle it here 
    if ((*env)->ExceptionOccurred(env)) { 
     (*env)->ExceptionDescribe(env); 
     (*env)->ExceptionClear(env); 
     Log_e("calling setHasAlpha threw an exception"); 
     return -2; 
    } 

    if(AndroidBitmap_unlockPixels(env, bitmap) < 0) { 
     Log_e("Failed to unlock the pixels of the Bitmap"); 
     return -1; 
    } 

    return 0; // success 
} 

Eso es todo. Hemos terminado. Publiqué todo el código para copiar y pegar. El código real no es tan grande, pero hacer todos estos controles de error paranoides lo hace mucho más grande. Espero que esto pueda ser útil para cualquiera.

0

Por lo tanto, createScaledBitmap y createBitmap (con matriz que se escala) en mapa de bits inmutables (como cuando se decodifica) ignorará Bitmap.Config original y creará bitmap con Bitmap.Config.ARGB_565 si el original no tiene ninguna transparencia (hasAlpha == false). Pero no lo hará en mapa de bits mutable. tanto, si su mapa de bits decodificado es b:

Bitmap temp = Bitmap.createBitmap(b.getWidth(), b.getHeight(), Bitmap.Config.ARGB_8888); 
Canvas canvas = new Canvas(temp); 
canvas.drawBitmap(b, 0, 0, null); 
b.recycle(); 

Ahora puede cambiar la escala de temperatura y se deben conservar Bitmap.Config.ARGB_8888.

3

Creé una biblioteca simple basada en littleFluffyKitty respuesta que cambia de tamaño y hace otras cosas como recorte y rotación, así que por favor úsela para usarla y mejorarla - Android-ImageResizer.

0

La escala de la imagen también se puede lograr de esta manera sin pérdida de calidad.

 //Bitmap bmp passed to method... 

     ByteArrayOutputStream stream = new ByteArrayOutputStream(); 
     bmp.compress(Bitmap.CompressFormat.JPEG, 100, stream);   
     Image jpg = Image.getInstance(stream.toByteArray());   
     jpg.scalePercent(68); // or any other number of useful methods. 
+0

¿Qué es la clase de imagen? No conozco una clase de ese nombre. Gracias – cottonBallPaws

+1

Oop's - mi mal, debería haber indicado que es parte de la fuente/biblioteca droidText o iText. Yo también uso para guardar mis imágenes en pdf. La clase de imagen proporciona una gran cantidad de métodos útiles. Quizás siendo ese droidText es openSource puedes extraer únicamente la clase Image para la manipulación de la Imagen. ¡La calidad de escalado es puntual! – mbp

1
onScreenResults = Bitmap.createScaledBitmap(tempBitmap, scaledOSRW, scaledOSRH, true); <---- 

ajustar el filtro a la verdadera trabajó para mí.

+0

Lo mismo aquí, cambiar el indicador de falso a verdadero solucionó mi problema. –

Cuestiones relacionadas