Schlechte Bildqualität nach Größenänderung / Skalierung der Bitmap


75

Ich schreibe ein Kartenspiel und muss meine Karten unter verschiedenen Umständen unterschiedlich groß haben. Ich speichere meine Bilder als Bitmaps, damit sie schnell gezeichnet und neu gezeichnet werden können (zur Animation).

Mein Problem ist, dass meine Bilder, egal wie ich sie skaliere (ob über eine matrix.postScale-, eine matrix.preScale- oder eine createScaledBitmap-Funktion), immer pixelig und verschwommen erscheinen. Ich weiß, dass es die Skalierung ist, die das Problem verursacht, da die Bilder beim Zeichnen ohne Größenänderung perfekt aussehen.

Ich habe jede in diesen beiden Threads beschriebene Lösung durchgearbeitet:
Android-Qualität der Bilder, deren Größe bei Problemen mit der Laufzeitqualität geändert wurde, wenn die Größe
eines Bildes zur Laufzeit geändert wurde

aber immer noch nirgendwo angekommen.

Ich speichere meine Bitmaps (in einer Hashmap) mit folgendem Code:

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

und zeichne sie mit dieser Methode (in einer Kartenklasse):

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

Jeder Einblick wäre sehr dankbar. Vielen Dank


Wenn Sie sagen "Dieser Code macht absolut keinen Unterschied", gehe ich davon aus, dass Sie den Parameter für setAntiAliasauf true setzen (und es macht immer noch keinen Unterschied)?
Fredley

Das ist richtig, ich habe mit wahr / falsch für all diese Methoden experimentiert und keine von ihnen macht einen Unterschied
Cbas

Diese Frage wurde auf Meta SE
Linny

Antworten:


45

Ich hatte unscharfe Bilder bei niedrigen Bildschirmauflösungen, bis ich die Skalierung beim Laden der Bitmap aus Ressourcen deaktivierte:

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

Das hat perfekt funktioniert! Kristallklare Bilder, keine Unschärfe. Vielen Dank. Ich habe festgestellt, dass ich auch noch die WarrenFaith-Methode verwenden muss: Durch Filtern des Bildes wird die Zackigkeit beseitigt, und durch Setzen der Option inScaled auf false wird die Unschärfe beseitigt. Vielen Dank für die Hilfe an alle!
Cbas

3
Tolles Zeug. Nur ein Hinweis, dass a.getResources () auf getResources () gekürzt werden kann und path die R.drawable.id der Bitmap ist.
Shadoath

Das hat super geklappt. Ich habe diese Zeile kommentiert o2.inSampleSize = scale; und fügte diese Zeile gemäß Ihrem Vorschlag hinzu (o2.inScaled = false;). Danke. Aber ich habe den Skalenwert trotzdem auf 1 gesetzt. Warum ist das Problem dann aufgetreten?
Sagar Devanga

88

Wenn Sie createScaledBitmap verwenden, sieht Ihr Bild sehr schlecht aus. Ich habe dieses Problem getroffen und es gelöst. Der folgende Code behebt das Problem:

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;

    }

Genial! Du bist ein Genie! Vielen Dank: D
Aberaud

6
Diese Lösung ist perfekt (es ist die einzige, die für mich funktioniert), aber Sie können loswerden middleXund middleY: setzen Sie einfach 0 und geben Sie ancanvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG));
aberaud

4
Ich habe meine Bitmaps mit einer anderen Farbe als Paint.FILTER_BITMAP_FLAGS gezeichnet. Der Wechsel zu Paint.FILTER_BITMAP_FLAG hat meine Ergebnisse drastisch verbessert! Vielen Dank!
Dror

Die Ergebnisse sind ziemlich verbessert, aber manchmal funktioniert die Methode nicht, wenn ich versucht habe, diese Methode direkt nach der Kameraaufnahme zu verwenden.
AnkitRox

4
Tut mir leid zu sagen, aber diese Lösung funktioniert in meinem Fall nicht.
sumeet

34

createScaledBitmaphat ein Flag, mit dem Sie festlegen können, ob das skalierte Bild gefiltert werden soll oder nicht. Dieses Flag verbessert die Qualität der Bitmap ...


1
Wow, das hat es sehr verbessert! Danke, ich kann nicht glauben, dass niemand das auf den anderen Threads erwähnt hat ... Es hat alle scharfen Kanten entfernt, aber die Bilder sind immer noch sehr verschwommen. Irgendwelche Ideen, wie man die Unschärfe loswird?
Cbas

11

benutzen als

mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); 

Paint.FILTER_BITMAP_FLAG ist Arbeit für mich


1
Ich weiß nicht, warum dies abgelehnt wurde, es war die Lösung für mich.
Stephane Mathis

2
Danke, das hat mir den Tag gerettet! Ich habe es als Argument für die canvas.drawBitmap-Methode verwendet
Couitchy

8

Ich gehe davon aus, dass Sie Code für eine Android-Version unter 3.2 (API-Stufe <12) schreiben, da seitdem das Verhalten der Methoden

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

hat sich verändert.

Auf älteren Plattformen (API-Ebene <12) versuchen die BitmapFactory.decodeFile (..) -Methoden standardmäßig, eine Bitmap mit RGB_565-Konfiguration zurückzugeben, wenn sie kein Alpha finden, was die Qualität eines iamge verringert. Dies ist immer noch in Ordnung, da Sie eine ARGB_8888-Bitmap mit erzwingen können

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

Das eigentliche Problem tritt auf, wenn jedes Pixel Ihres Bildes einen Alpha-Wert von 255 hat (dh vollständig undurchsichtig). In diesem Fall wird das Bitmap-Flag 'hasAlpha' auf false gesetzt, obwohl Ihre Bitmap die Konfiguration ARGB_8888 hat. Wenn Ihre * .png-Datei mindestens ein echtes transparentes Pixel hätte, wäre dieses Flag auf true gesetzt worden und Sie müssten sich um nichts kümmern.

Wenn Sie also eine skalierte Bitmap mit erstellen möchten

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

Die Methode prüft, ob das Flag 'hasAlpha' auf true oder false gesetzt ist. In Ihrem Fall wird es auf false gesetzt. Dadurch wird eine skalierte Bitmap erhalten, die automatisch in das Format RGB_565 konvertiert wurde.

Daher gibt es auf API-Ebene> = 12 eine öffentliche Methode namens

public void setHasAlpha (boolean hasAlpha);

das hätte dieses Problem gelöst. Bisher war dies nur eine Erklärung des Problems. Ich habe einige Nachforschungen angestellt und festgestellt, dass die setHasAlpha-Methode schon lange existiert und öffentlich ist, aber ausgeblendet wurde (Anmerkung @hide). So ist es in Android 2.3 definiert:

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

Hier ist mein Lösungsvorschlag. Es werden keine Bitmap-Daten kopiert:

  1. Zur Laufzeit mit java.lang.Reflect überprüft, ob die aktuelle Bitmap-Implementierung eine öffentliche 'setHasAplha'-Methode hat. (Laut meinen Tests funktioniert es perfekt seit API Level 3, und ich habe keine niedrigeren Versionen getestet, weil JNI nicht funktionieren würde). Sie können Probleme haben, wenn ein Hersteller es ausdrücklich privat gemacht, geschützt oder gelöscht hat.

  2. Rufen Sie die 'setHasAlpha'-Methode für ein bestimmtes Bitmap-Objekt mit JNI auf. Dies funktioniert perfekt, auch für private Methoden oder Felder. Es ist offiziell, dass JNI nicht prüft, ob Sie gegen die Zugriffssteuerungsregeln verstoßen oder nicht. Quelle: http://java.sun.com/docs/books/jni/html/pitfalls.html (10.9) Dies gibt uns große Kraft, die mit Bedacht eingesetzt werden sollte. Ich würde nicht versuchen, ein endgültiges Feld zu ändern, selbst wenn es funktionieren würde (nur um ein Beispiel zu geben). Und bitte beachten Sie, dass dies nur eine Problemumgehung ist ...

Hier ist meine Implementierung aller notwendigen Methoden:

JAVA TEIL:

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

Laden Sie Ihre Bibliothek und deklarieren Sie die native Methode:

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

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

Native Sektion ('jni' Ordner)

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
}

Das ist es. Wir sind fertig. Ich habe den gesamten Code zum Kopieren und Einfügen veröffentlicht. Der eigentliche Code ist nicht so groß, aber all diese paranoiden Fehlerprüfungen machen ihn viel größer. Ich hoffe das könnte für jeden hilfreich sein.


Können Sie bitte die obigen Codes in ein Beispiel-Android-Projekt in GitHub einfügen? Ihr Ansatz ist interessant.
SMMousavi

Entschuldigung, aber ich habe kein Setup mehr für die native Entwicklung.
Ivo

8

Ein guter Downscaling-Algorithmus (nicht wie beim nächsten Nachbarn, daher wird keine Pixelung hinzugefügt) besteht aus nur 2 Schritten (plus Berechnung des genauen Rect für das Zuschneiden von Eingabe- / Ausgabebildern):

  1. Downscale mit BitmapFactory.Options :: inSampleSize -> BitmapFactory.decodeResource () so nah wie möglich an der Auflösung, die Sie benötigen, aber nicht weniger als diese
  2. Erreichen Sie die genaue Auflösung, indem Sie mit Canvas :: drawBitmap () ein wenig verkleinern.

Hier finden Sie eine ausführliche Erklärung, wie SonyMobile diese Aufgabe gelöst hat: https://web.archive.org/web/20171011183652/http://developer.sonymobile.com/2011/06/27/how-to-scale-images-for- Ihre-Android-Anwendung /

Hier ist der Quellcode der SonyMobile-Scale-Utils: https://web.archive.org/web/20170105181810/http://developer.sonymobile.com:80/downloads/code-example-module/image-scaling-code- Beispiel für Android /


4

Sie werden nie ein perfektes Ergebnis erzielen, wenn Sie Ihre Bitmaps vergrößern.

Sie sollten mit der höchsten Auflösung beginnen, die Sie benötigen, und verkleinern.

Beim Skalieren einer Bitmap kann die Skalierung nicht erraten, welche Punkte zwischen den einzelnen vorhandenen Punkten fehlen. Daher wird entweder ein Nachbar dupliziert (=> nervös) oder ein Mittelwert zwischen Nachbarn berechnet (=> verschwommen).


Das ist absolut sinnvoll, obwohl ich mit den oben beschriebenen Methoden ein sehr klares Bild erhalten habe - so klar, dass ich keinen Unterschied zwischen dem realen und dem vergrößerten Bild erkennen kann. Ich bin mir sicher, dass es nicht perfekt ist, aber es sieht definitiv so aus (und ich benutze eines der neuesten Android-Modelle, um es zu testen). Gültiger Punkt. Ich muss das auf jeden Fall berücksichtigen, wenn ich dieses Spiel für ein Tablet veröffentlichen
möchte

4

Ich habe gerade Flagge verwendet filter=truein bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true); für Unschärfe.


1

Wenn Sie qualitativ hochwertige Ergebnisse erzielen möchten, verwenden Sie die Bibliothek [RapidDecoder] [1]. Es ist einfach wie folgt:

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

Vergessen Sie nicht, den eingebauten Decoder zu verwenden, wenn Sie weniger als 50% und ein HQ-Ergebnis erzielen möchten. Ich habe es auf API 8 getestet.


1

Hatte dieses Problem beim Aktualisieren von Android Target Framework von Android 8.1 auf Android 9 und manifestierte sich auf meinem ImageEntryRenderer. Hoffe das hilft

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

Hinweis: Ich entwickle unter Xamarin 3.4.0.10


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

    }
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.