77 votes

Mauvaise qualité d'image après le redimensionnement/la mise à l'échelle d'un bitmap

J'écris un jeu de cartes et j'ai besoin que mes cartes aient des tailles différentes selon les circonstances. Je stocke mes images sous forme de bitmaps afin qu'elles puissent être rapidement dessinées et redessinées (pour l'animation).

Le problème est que, quelle que soit la façon dont j'essaie de mettre mes images à l'échelle (que ce soit par le biais d'une matrice.postScale, d'une matrice.preScale ou d'une fonction createScaledBitmap), elles sont toujours pixellisées et floues. Je sais que c'est la mise à l'échelle qui est à l'origine du problème car les images sont parfaites lorsqu'elles sont dessinées sans être redimensionnées.

J'ai essayé toutes les solutions décrites dans ces deux fils de discussion :
Qualité Android des images redimensionnées en cours d'exécution
problèmes de qualité lors du redimensionnement d'une image en cours d'exécution

mais je n'ai toujours rien obtenu.

Je stocke mes bitmaps (dans un hashmap) avec ce code :

cardImages = new HashMap<Byte, Bitmap>();
cardImages.put(GameUtil.hearts_ace, BitmapFactory.decodeResource(r, R.drawable.hearts_ace));

et les dessiner avec cette méthode (dans une classe de carte) :

public void drawCard(Canvas c)
{
    //retrieve the cards image (if it doesn't already have one)
    if (image == null)
        image = Bitmap.createScaledBitmap(GameUtil.cardImages.get(ID), 
            (int)(GameUtil.standardCardSize.X*scale), (int)(GameUtil.standardCardSize.Y*scale), false);

        //this code (non-scaled) looks perfect
        //image = GameUtil.cardImages.get(ID);

    matrix.reset();
    matrix.setTranslate(position.X, position.Y);

    //These methods make it look worse
    //matrix.preScale(1.3f, 1.3f);
    //matrix.postScale(1.3f, 1.3f);

    //This code makes absolutely no difference
    Paint drawPaint = new Paint();
    drawPaint.setAntiAlias(false);
    drawPaint.setFilterBitmap(false);
    drawPaint.setDither(true);

    c.drawBitmap(image, matrix, drawPaint);
}

Tout commentaire serait grandement apprécié. Merci

0 votes

Lorsque vous dites "Ce code ne fait absolument aucune différence", je suppose que vous définissez le paramètre pour les éléments suivants setAntiAlias à true (et cela ne fait toujours aucune différence) ?

0 votes

C'est correct, j'ai expérimenté le vrai/faux pour toutes ces méthodes et aucune ne fait de différence.

0 votes

Cette question a été mentionnée sur Meta SE

89voto

le huynh Points 271

L'utilisation de createScaledBitmap rendra votre image très mauvaise. J'ai rencontré ce problème et je l'ai résolu. Le code ci-dessous va résoudre le problème :

public Bitmap BITMAP_RESIZER(Bitmap bitmap,int newWidth,int newHeight) {    
    Bitmap scaledBitmap = Bitmap.createBitmap(newWidth, newHeight, Config.ARGB_8888);

    float ratioX = newWidth / (float) bitmap.getWidth();
    float ratioY = newHeight / (float) bitmap.getHeight();
    float middleX = newWidth / 2.0f;
    float middleY = newHeight / 2.0f;

    Matrix scaleMatrix = new Matrix();
    scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);

    Canvas canvas = new Canvas(scaledBitmap);
    canvas.setMatrix(scaleMatrix);
    canvas.drawBitmap(bitmap, middleX - bitmap.getWidth() / 2, middleY - bitmap.getHeight() / 2, new Paint(Paint.FILTER_BITMAP_FLAG));

    return scaledBitmap;

    }

0 votes

Génial ! Tu es un génie ! Merci beaucoup :D

6 votes

Cette solution est parfaite (c'est la seule qui fonctionne pour moi), cependant vous pouvez vous débarrasser de middleX y middleY : il suffit de mettre 0 et l'état canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG));

4 votes

J'avais l'habitude de dessiner mes bitmaps avec une peinture différente de Paint.FILTER_BITMAP_FLAGS. Le passage à Paint.FILTER_BITMAP_FLAG a considérablement amélioré mes résultats ! Merci !

45voto

Lumis Points 10300

J'avais des images floues sur des résolutions d'écran faibles jusqu'à ce que je désactive la mise à l'échelle sur le chargement des bitmaps à partir des ressources :

Options options = new BitmapFactory.Options();
    options.inScaled = false;
    Bitmap source = BitmapFactory.decodeResource(a.getResources(), path, options);

0 votes

Cela a parfaitement fonctionné ! Des images claires comme du cristal, pas de flou. Merci.. J'ai découvert que j'ai toujours besoin d'utiliser la méthode de WarrenFaith : filtrer l'image permet de se débarrasser des irrégularités, et mettre l'option inScaled à false permet de se débarrasser du flou. Merci à tous pour votre aide !

3 votes

Excellent travail. Juste une remarque : a.getResources() peut être raccourci en getResources() et path est le R.drawable.id du bitmap.

0 votes

Cela a bien fonctionné. J'ai commenté cette ligne o2.inSampleSize=scale ; et ajouté cette ligne selon votre suggestion ( o2.inScaled = false ; ). Merci. Mais j'avais de toute façon fixé la valeur de l'échelle à 1. Alors pourquoi le problème s'est-il produit, pouvez-vous m'expliquer ?

35voto

WarrenFaith Points 28137

createScaledBitmap a un drapeau où vous pouvez définir si l'image mise à l'échelle doit être filtrée ou non. Ce drapeau améliore la qualité du bitmap...

1 votes

Wow, ça l'a beaucoup amélioré ! Merci, je n'arrive pas à croire que personne ne l'ait mentionné dans les autres fils de discussion... J'ai enlevé tous les angles vifs, mais les images sont encore très floues. Avez-vous des idées sur la façon de se débarrasser du flou ?

8voto

Ivo Points 940

Je suppose que vous écrivez du code pour une version d'Android inférieure à 3.2 (niveau 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(..) essaient de renvoyer un bitmap avec une 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 imposer un bitmap ARGB_8888 en utilisant

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

Le véritable problème se pose lorsque chaque pixel de votre image a une valeur alpha de 255 (c'est-à-dire qu'il est complètement opaque). Dans ce cas, l'indicateur 'hasAlpha' du bitmap est mis à false, même si votre bitmap a une configuration ARGB_8888. Si votre fichier *.png avait au moins un pixel réellement transparent, cet indicateur aurait été mis à true et vous n'auriez pas à vous soucier de quoi que ce soit.

Ainsi, lorsque vous voulez créer un bitmap à 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 false, et dans votre cas, il est défini sur false, ce qui permet d'obtenir un Bitmap mis à l'échelle, qui a été automatiquement converti au format RGB_565.

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

public void setHasAlpha (boolean hasAlpha);

ce qui aurait permis de résoudre ce problème. Jusqu'à présent, il ne s'agissait que d'une explication du problème. J'ai fait quelques recherches et j'ai remarqué que la méthode setHasAlpha existe depuis longtemps et qu'elle est publique, mais qu'elle a été cachée (annotation @hide). Voici comment elle est définie 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);
}

Voici maintenant ma proposition de solution. Elle n'implique aucune copie de données bitmap :

  1. Vérifié au moment de l'exécution à l'aide de java.lang.Reflect si l'implémentation actuelle du Bitmap possède une méthode publique 'setHasAplha'. (Selon mes tests, cela 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é en utilisant JNI. Cela fonctionne parfaitement, même pour les méthodes ou les champs privés. Il est officiel que JNI ne vérifie pas si vous violez ou non les règles de contrôle d'accès. Source : http://java.sun.com/docs/books/jni/html/pitfalls.html (10.9) Cela nous donne un grand pouvoir, qui doit être utilisé à bon escient. Je n'essaierais pas de modifier un champ final, même si cela pouvait fonctionner (juste pour donner un exemple). Et notez bien qu'il ne s'agit que d'une solution de rechange...

Voici mon implémentation 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 librairie 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 terminé. J'ai posté le code entier pour le copier-coller. Le code actuel n'est pas si gros, mais le fait de faire tous ces contrôles d'erreurs paranoïaques le rend beaucoup plus gros. J'espère que cela pourra être utile à tous.

0 votes

Pouvez-vous ajouter les codes ci-dessus dans un exemple de projet Android dans GitHub ? Votre approche est intéressante.

0 votes

Désolé, mais je n'ai plus la configuration pour le développement natif.

8voto

goRGon Points 291

Un bon algorithme de réduction d'échelle (qui n'est pas du type "nearest neighbor", de sorte qu'aucune pixellisation n'est ajoutée) ne comprend que deux étapes (plus le calcul du rectangle exact pour le recadrage des images d'entrée/sortie) :

  1. réduire l'échelle en utilisant BitmapFactory.Options::inSampleSize -> BitmapFactory.decodeResource() aussi proche que possible de la résolution dont vous avez besoin, mais pas inférieure à celle-ci
  2. d'obtenir la résolution exacte en réduisant un peu l'échelle en utilisant Canvas::drawBitmap()

Voici une explication détaillée de la manière dont SonyMobile a résolu cette tâche : https://web.archive.org/web/20171011183652/http://developer.sonymobile.com/2011/06/27/how-to-scale-images-for-your-Android-application/

Voici le code source de SonyMobile scale utils : https://web.archive.org/web/20170105181810/http://developer.sonymobile.com:80/downloads/code-example-module/image-scaling-code-example-for-Android/

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X