web-dev-qa-db-fra.com

Mauvaise qualité d'image après le redimensionnement/redimensionnement de l'image bitmap

J'écris un jeu de cartes et j'ai besoin que mes cartes soient de 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).

Mon problème est que peu importe la façon dont j'essaie de redimensionner mes images (que ce soit à l'aide d'une matrice. Je sais que c'est la mise à l'échelle qui pose problème car les images sont parfaites lorsqu'elles sont dessinées sans redimensionnement. 

J'ai travaillé à travers chaque solution décrite dans ces deux threads:
Qualité Android des images redimensionnées au moment de l'exécution
problèmes de qualité lors du redimensionnement d'une image au moment de l'exécution

mais toujours pas eu n'importe où. 

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

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

et dessinez-les 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);
}

Toute idée serait grandement apprécié. Merci

72
Cbas

J'avais des images floues sur des résolutions d'écran basses jusqu'à ce que je désactive la mise à l'échelle sur une charge bitmap depuis des ressources:

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

Utilisez createScaledBitmap pour rendre votre image très mauvaise . J'ai rencontré ce problème et je l'ai résolu . Le code ci-dessous résoudra 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;

    }
79
le huynh

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

33
WarrenFaith

utilisé comme

mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); 

Paint.FILTER_BITMAP_FLAG est un travail pour moi 

10
chuckwu

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 'setHasAplha' publique. (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é en utilisant JNI . Cela fonctionne parfaitement, même pour des méthodes ou des champs privés. 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.

8
Ivo

Un bon algorithme de réduction d’échelle (pas de voisin proche, donc aucune pixelisation n’est ajoutée) consiste en seulement 2 étapes (plus le calcul du Rect exact pour le recadrage des images d’entrée/sortie):

  1. utilisez une échelle inférieure en utilisant BitmapFactory.Options :: inSampleSize -> BitmapFactory.decodeResource () aussi proche que possible de la résolution dont vous avez besoin
  2. obtenez la résolution exacte en réduisant un peu la taille à l’aide de 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- pour-votre-application-Android/

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

7
goRGon

Vous n'obtiendrez jamais un résultat parfait si vous redimensionnez vos bitmaps.

Vous devez commencer avec la résolution la plus élevée dont vous avez besoin et réduire la taille.

Lors de la mise à l'échelle d'une image bitmap, la mise à l'échelle ne peut pas deviner les points manquants entre chaque point existant. Elle duplique donc un voisin (=> énervé) ou calcule une valeur moyenne entre voisins (=> floue).

4
Kevin Gaudin

Je viens d'utiliser l'indicateur filter=true dans bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true); Pour le flou.

3

Si vous voulez un résultat de haute qualité, utilisez donc la bibliothèque [RapidDecoder] [1]. C'est simple comme suit:

import rapid.decoder.BitmapDecoder;
...
Bitmap bitmap = BitmapDecoder.from(getResources(), R.drawable.image)
                             .scale(width, height)
                             .useBuiltInDecoder(true)
                             .decode();

N'oubliez pas d'utiliser le décodeur intégré si vous souhaitez réduire moins de 50% et obtenir un résultat HQ. Je l'ai testé sur API 8.

1
S.M.Mousavi

Ce problème est survenu lors de la mise à jour de Android Target Framework de Android 8.1 à Android 9 et s'est manifesté sur mon ImageEntryRenderer. J'espère que cela t'aides

    public Bitmap ProcessScaleBitMap(Bitmap bitmap, int newWidth, int newHeight)
    {
        newWidth = newWidth * 2;
        newHeight = newHeight * 2;

        Bitmap scaledBitmap = CreateBitmap(newWidth, newHeight, Config.Argb8888);

        float scaleDensity = ((float)Resources.DisplayMetrics.DensityDpi / 160);
        float scaleX = newWidth / (bitmap.Width * scaleDensity);
        float scaleY = newHeight / (bitmap.Height * scaleDensity);

        Matrix scaleMatrix = new Matrix();
        scaleMatrix.SetScale(scaleX, scaleY);

        Canvas canvas = new Canvas(scaledBitmap);
        canvas.Matrix = scaleMatrix;
        canvas.DrawBitmap(bitmap, 0, 0, new Paint(PaintFlags.FilterBitmap));

        return scaledBitmap;
    }

Note: Je développe sous le framework Xamarin 3.4.0.10

1
Aldous Realo
private static Bitmap createScaledBitmap(Bitmap bitmap,int newWidth,int newHeight) {
        Bitmap scaledBitmap = Bitmap.createBitmap(newWidth, newHeight, bitmap.getConfig());

        float scaleX = newWidth / (float) bitmap.getWidth();
        float scaleY = newHeight / (float) bitmap.getHeight();

        Matrix scaleMatrix = new Matrix();
        scaleMatrix.setScale(scaleX, scaleY, 0, 0);

        Canvas canvas = new Canvas(scaledBitmap);
        canvas.setMatrix(scaleMatrix);
        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
        Paint.setAntiAlias(true);
        Paint.setDither(true);
        Paint.setFilterBitmap(true);
        canvas.drawBitmap(bitmap, 0, 0, Paint);

        return scaledBitmap;

    }
0
Gil SH