web-dev-qa-db-fra.com

Problèmes de qualité lors du redimensionnement d'une image au moment de l'exécution

J'ai un fichier image sur le disque, je le redimensionne et le sauvegarde sur le disque en tant que nouveau fichier image. Pour les besoins de cette question, je ne les mets pas en mémoire pour les afficher à l'écran, mais uniquement pour les redimensionner et les enregistrer à nouveau. Tout cela fonctionne très bien. Cependant, les images redimensionnées comportent des artefacts, comme indiqué ci-dessous: Android: qualité des images redimensionnées au moment de l'exécution

Ils sont sauvegardés avec cette distorsion, car je peux les retirer du disque et les regarder sur mon ordinateur et ils ont toujours le même problème.

J'utilise un code similaire à ceci Étrange problème de mémoire insuffisante lors du chargement d'une image dans un objet Bitmap pour décoder le bitmap en mémoire:

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);

Ensuite, je fais la mise à l'échelle réelle avec:

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

Enfin, la nouvelle image redimensionnée est enregistrée sur le disque avec:

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

Ensuite, comme je l'ai mentionné, si je retire ce fichier du disque et l'examine, le problème de qualité associé est exposé ci-dessus et il a une mine terrible. Si je saute le createScaledBitmap et que je sauvegarde juste le sampledSrcBitmap sur le disque, il n'y a pas de problème, cela ne semble se produire que si la taille change.

Comme vous pouvez le constater dans le code, j'ai essayé de définir inDither sur false comme indiqué ici http://groups.google.com/group/Android-developers/browse_thread/thread/8b1abdbe881f9f71 et comme mentionné dans le même premier post lié ci-dessus. Cela n'a rien changé. De plus, dans le premier message que j'ai lié, Romain Guy a déclaré: 

Au lieu de redimensionner au moment du dessin (ce qui va être très coûteux), essayez de redimensionner dans un bitmap hors écran et assurez-vous que Bitmap est 32 bits (ARGB888).

Cependant, je ne sais pas comment m'assurer que le bitmap reste 32 bits tout au long du processus.

J'ai également lu quelques autres articles tels que celui-ci http://Android.nakatome.net/2010/04/bitmap-basics.html mais ils semblaient tous s'adresser au dessin et à l'affichage du bitmap, je veux juste pour le redimensionner et le sauvegarder sur le disque sans ce problème de qualité.

Merci beaucoup

27
cottonBallPaws

Après des expériences, j'ai enfin trouvé un moyen de le faire avec des résultats de bonne qualité. Je rédigerai ceci pour tous ceux qui pourraient trouver cette réponse utile dans l’avenir.

Pour résoudre le premier problème, les artefacts et le tremblement étrange introduits dans les images, vous devez vous assurer que votre image reste sous la forme d'une image ARGB_8888 32 bits. En utilisant le code de ma question, vous pouvez simplement ajouter cette ligne aux options avant le deuxième décodage.

options.inPreferredConfig = Bitmap.Config.ARGB_8888;

Après avoir ajouté cela, les artefacts avaient disparu, mais les contours des images passaient en dents de scie au lieu d'être nets. Après quelques expériences supplémentaires, j'ai découvert que le redimensionnement du bitmap en utilisant une matrice au lieu de Bitmap.createScaledBitmap produisait des résultats beaucoup plus précis.

Avec ces deux solutions, les images sont maintenant parfaitement redimensionnées. Vous trouverez ci-dessous le code que j'utilise au cas où cela profiterait à une autre personne rencontrant ce problème.

// 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: Après un travail continu sur ce sujet, j'ai constaté que les images n'étaient toujours pas parfaites à 100%. Je ferai une mise à jour si je peux l'améliorer.

Mise à jour: Après avoir annulé cette modification, j'ai trouvé cette question sur SO et une réponse mentionnant l'option inScaled. Cela a également contribué à la qualité. J'ai donc mis à jour la réponse ci-dessus pour l'inclure. J'annule aussi maintenant les bitmaps après leur utilisation.

En outre, si vous utilisez ces images dans une vue Web, n'oubliez pas de prendre en compte cet article.

Remarque: vous devez également ajouter une coche pour vous assurer que la largeur et la hauteur sont des nombres valides (et non -1). Si tel est le cas, la boucle inSampleSize deviendra infinie.

54
cottonBallPaws

Dans ma situation, je dessine l'image à l'écran. Voici ce que j'ai fait pour que mes images soient correctes (combinaison de la réponse de littleFluffyKitty et de quelques autres éléments).

Pour mes options lorsque je charge réellement l'image (à l'aide de decodeResource), je définis les valeurs suivantes:

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

Lorsque je dessine réellement l'image, je configure mon objet Paint comme suit:

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

Espérons que quelqu'un d'autre trouve cela utile aussi. J'aimerais qu'il y ait juste des options pour "Oui, laissez mes images redimensionnées ressembler à des déchets" et "Non, veuillez ne pas forcer mes utilisateurs à se griller les yeux avec des cuillères" au lieu de toute la myriade d'options différentes. Je sais qu'ils veulent nous donner beaucoup de contrôle, mais peut-être que des méthodes d'assistance pour les paramètres communs pourraient être utiles.

8
Jon Turner

J'ai créé une bibliothèque simple basée sur la réponse littleFluffyKitty qui redimensionne et effectue d'autres opérations telles que le recadrage et la rotation. Veuillez donc l'utiliser et l'améliorer - Android-ImageResizer .

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

définir le filtre sur true a fonctionné pour moi.

2
loco

"Cependant, je ne sais pas comment m'assurer que le bitmap reste en 32 bits Tout au long du processus."

Je voulais poster une solution alternative, qui permet de garder la configuration ARGB_8888 intacte. REMARQUE: ce code ne décode que les bitmaps et doit être étendu pour que vous puissiez stocker un bitmap.

Je suppose que vous écrivez du code pour une version d'Android inférieure à 3.2 (niveau d'API <12), car depuis lors, le comportement des méthodes

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

a changé.

Sur les anciennes plates-formes (niveau d'API <12), les méthodes BitmapFactory.decodeFile (..) tentent de renvoyer un bitmap avec la configuration RGB_565 par défaut, si elles ne trouvent pas d'alpha, ce qui diminue la qualité d'un iamge. Cela reste correct, car vous pouvez appliquer un bitmap ARGB_8888 en utilisant

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

Le vrai problème vient du fait que chaque pixel de votre image a une valeur alpha de 255 (c’est-à-dire complètement opaque). Dans ce cas, l'indicateur de la bitmap 'hasAlpha' est défini sur false, même si votre bitmap a la configuration ARGB_8888. Si votre fichier * .png avait au moins un véritable pixel transparent, cet indicateur aurait été mis à true et vous n'auriez à vous soucier de rien. 

Alors, quand vous voulez créer un bitmap mis à l'échelle en utilisant

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

la méthode vérifie si l'indicateur 'hasAlpha' est défini sur true ou sur false et, dans votre cas, sur false, ce qui entraîne l'obtention d'un bitmap mis à l'échelle, qui a été automatiquement converti au format RGB_565.

Par conséquent, au niveau de l'API> = 12, il existe une méthode publique appelée

public void setHasAlpha (boolean hasAlpha);

qui aurait résolu ce problème. Jusqu'ici, il ne s'agissait que d'une explication du problème ... J'ai fait quelques recherches et constaté que la méthode setHasAlpha existe depuis longtemps et qu'elle est publique, mais qu'elle a été masquée (annotation @hide). Voici comment cela est défini sur 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);
}

Maintenant, voici ma proposition de solution. Cela n'implique aucune copie de données bitmap:

  1. Vérifié lors de l'exécution à l'aide de Java.lang.Reflect si l'implémentation Bitmap actuelle a une méthode publique 'setHasAplha'. (D'après mes tests, il fonctionne parfaitement depuis le niveau 3 de l'API et je n'ai pas testé les versions inférieures, car JNI ne fonctionnerait pas). Vous pouvez avoir des problèmes si un fabricant l'a explicitement rendu privé, protégé ou supprimé.

  2. Appelez la méthode 'setHasAlpha' pour un objet Bitmap donné à l'aide de JNI . Cela fonctionne parfaitement, même pour les méthodes privées ou les champs. Il est officiel que JNI ne vérifie pas si vous violez les règles de contrôle d’accès ou non . Source: http://Java.Sun.com/docs/books/jni/html/pitfalls.html ( 10.9) Cela nous donne un grand pouvoir, qui devrait être utilisé à bon escient. Je ne voudrais pas essayer de modifier un dernier champ, même si cela fonctionnerait (juste pour donner un exemple). Et s'il vous plaît noter que ceci est juste une solution de contournement ...

Voici ma mise en œuvre de toutes les méthodes nécessaires:

Java PART:

// 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;
    }

Chargez votre lib et déclarez la méthode native:

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

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

Section native (dossier '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
}

C'est tout. Nous avons fini. J'ai posté tout le code à des fins de copier-coller ... Le code réel n'est pas si gros, mais faire toutes ces vérifications d'erreur paranoïdes le rend beaucoup plus gros. J'espère que cela pourrait être utile à n'importe qui.

2
Ivo

La mise à l'échelle de l'image peut également être réalisée de cette manière, sans aucune perte de qualité!

      //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
mbp

Donc, createScaledBitmap et createBitmap (avec une matrice qui évolue) sur un bitmap immuable (comme lors du décodage) ignoreront Bitmap.Config d'origine et créeront un bitmap avec Bitmap.Config.ARGB_565 si original n'a aucune transparence (hasAlpha == false) Mais il ne le fera pas sur un bitmap mutable . Donc, si votre bitmap décodé est 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();

Maintenant, vous pouvez redimensionner temp et il devrait conserver Bitmap.Config.ARGB_8888.

0
CloudWalker