Zła jakość obrazu po zmianie rozmiaru / skalowaniu bitmapy

Piszę grę karcianą i potrzebuję, aby moje karty były różnej wielkości w różnych okolicznościach. Moje obrazy przechowuję jako bitmapy, dzięki czemu można je szybko rysować i przerysowywać (na potrzeby animacji).

Mój problem polega na tym, że bez względu na to, jak próbuję skalować moje obrazy (czy to przez matrycę.postScale, matryca.preScale lub funkcja createScaledBitmap) zawsze wychodzą pikselowane i rozmyte. Wiem, że to skalowanie powoduje problem, ponieważ obrazy wyglądają idealnie po narysowaniu bez zmiany rozmiaru.

Przejrzałem wszystkie rozwiązania opisane w tych dwóch wątkach:
jakość obrazów zmienianych w czasie wykonywania na Androidzie
problemy z jakością podczas zmiany rozmiaru obrazu w czasie wykonywania

/ Align = "left" /

Zapisuję swoje bitmapy (do hashmapy) o tym kodzie:

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

I narysuj je tą metodą (w klasie kart):

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);
}
Każdy wgląd byłby bardzo mile widziany. Dzięki
Author: Community, 2011-01-27

10 answers

Miałem obrazy blury na niskich rozdzielczościach ekranu, dopóki nie wyłączyłem skalowania przy ładowaniu bitmap z zasobów:

Options options = new BitmapFactory.Options();
    options.inScaled = false;
    Bitmap source = BitmapFactory.decodeResource(a.getResources(), path, options);
 42
Author: Lumis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-27 22:01:35

Użycie createScaledBitmap sprawi, że twój obraz będzie wyglądał bardzo źle. Spotkałem się z tym problemem i go rozwiązałem. Poniższy kod naprawi 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;

    }
 77
Author: le huynh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-01 14:55:29

createScaledBitmap posiada flagę, w której można ustawić, czy przeskalowany obraz ma być filtrowany, czy nie. Flaga ta poprawia jakość bitmapy...

 32
Author: WarrenFaith,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-27 20:32:32

Użyj jako

mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); 

Paint.FILTER_BITMAP_FLAG is work for me

 9
Author: chuckwu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-07 09:21:50

Zakładam, że piszesz kod dla wersji Androida niższej niż 3.2( poziom API

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

Zmienił się.

Na starszych platformach (poziom API
options.inPrefferedConfig = Bitmap.Config.ARGB_8888
options.inDither = false 

Pojawia się prawdziwy problem gdy każdy piksel obrazu ma wartość alfa 255 (tj. całkowicie nieprzezroczysty). W takim przypadku flaga bitmapy 'hasAlpha' jest ustawiona na false, nawet jeśli bitmapy mają konfigurację ARGB_8888. Jeśli Twoje *.plik png miał co najmniej jeden prawdziwy przezroczysty piksel, flaga ta byłaby ustawiona na true i nie musisz się o nic martwić.

Więc jeśli chcesz utworzyć skalowaną bitmapę za pomocą

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

Metoda sprawdza, czy flaga 'hasAlpha' jest ustawiona na true czy false, a w Twoim przypadek jest ustawiony na false, co skutkuje uzyskaniem skalowanej bitmapy, która została automatycznie przekonwertowana do formatu RGB_565.

Dlatego na poziomie API > = 12 istnieje publiczna metoda o nazwie

public void setHasAlpha (boolean hasAlpha);
Co rozwiązałoby ten problem. Do tej pory było to tylko wyjaśnienie problemu. Poszperałem trochę i zauważyłem, że metoda setHasAlpha istnieje od dawna i jest publiczna, ale została ukryta (@hide adnotacja). Oto jak to jest zdefiniowane na Androidzie 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);
}
Oto moja propozycja rozwiązania. Nie wymaga kopiowania danych bitmapowych:
  1. Sprawdzane podczas pracy przy użyciu Javy.lang.Zastanów się, czy obecny Implementacja Bitmap ma publiczną metodę "setHasAplha". (Wg moich testów działa idealnie od poziomu API 3, a ja nie testowałem niższych wersji, bo JNI by nie zadziałało). Możesz mieć problemy, jeśli producent wyraźnie uczynił go prywatnym, chronionym lub usunął.

  2. Call metoda 'setHasAlpha' dla danego obiektu bitmapowego wykorzystującego JNI. Działa to doskonale, nawet dla prywatnych metod lub pól. To oficjalne, że JNI nie sprawdza, czy naruszasz zasady kontroli dostępu, czy nie. Źródło: http://java.sun.com/docs/books/jni/html/pitfalls.html (10, 9) To daje nam wielką moc, którą należy mądrze wykorzystać. Nie próbowałbym modyfikować finalnego pola, nawet gdyby zadziałało (tylko dla przykładu). I proszę pamiętać, że to tylko obejście...

Oto moja implementacja wszystkich niezbędnych metod:

CZĘŚĆ JAVA:

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

Załaduj lib i zadeklaruj natywną metodę:

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

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

Native section (folder '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
}
To wszystko. Skończyliśmy. Wysłałem cały kod do celów kopiowania i wklejania. Rzeczywisty kod nie jest tak duży, ale Dokonywanie tych wszystkich paranoicznych kontroli błędów sprawia, że jest o wiele większy. Mam nadzieję, że to może bądź pomocny dla każdego.
 8
Author: Ivo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-09-01 18:21:52

Dobry algorytm downscalingu (nie najbliższy sąsiad, więc nie jest dodawana pikselacja) składa się z zaledwie 2 kroków (plus obliczenie dokładnego Rect dla obrazów wejściowych/wyjściowych):

  1. w dół przy użyciu BitmapFactory.Opcje:: inSampleSize -> BitmapFactory.decodeResource() jak najbardziej zbliżona do wymaganej rozdzielczości, ale nie mniejsza od niej
  2. uzyskaj dokładną rozdzielczość, zmniejszając trochę za pomocą Canvas:: drawBitmap()

Oto szczegółowe wyjaśnienie, jak SonyMobile rozwiązał to zadanie: https://web.archive.org/web/20171011183652/http://developer.sonymobile.com/2011/06/27/how-to-scale-images-for-your-android-application/

Oto kod źródłowy SonyMobile scale utils: https://web.archive.org/web/20170105181810/http://developer.sonymobile.com:80/downloads/code-example-module/image-scaling-code-example-for-android/

 7
Author: goRGon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-25 12:41:09

Nigdy nie uzyskasz doskonałego wyniku, jeśli zwiększysz swoje bitmapy.

Powinieneś zacząć od najwyższej rozdzielczości, jakiej potrzebujesz i skalować w dół.

Podczas skalowania bitmapy w górę, skalowanie nie może odgadnąć, jakie są brakujące punkty między każdym istniejącym punktem, więc albo powiela sąsiada (=> ostry), albo oblicza średnią wartość między sąsiadami (=>rozmyty).

 4
Author: Kevin Gaudin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-01-27 23:14:41

Właśnie użyłem flagi filter=true w bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true); dla smugi.

 3
Author: Dmitrij Semenchuk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-03-06 14:23:35

Jeśli chcesz uzyskać wynik wysokiej jakości, Użyj biblioteki[RapidDecoder] [1]. Jest to proste w następujący sposób:

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

Nie zapomnij użyć wbudowanego dekodera, jeśli chcesz zmniejszyć skalę poniżej 50% i wynik HQ. Testowałem go na API 8.

 0
Author: S.M.Mousavi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-06 19:40:57
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
Author: Gil SH,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-16 15:47:54