Погана якість зображення після зміни розміру / масштабування растрового зображення


75

Я пишу картову гру, і мені потрібно, щоб картки були різного розміру за різних обставин. Я зберігаю свої зображення у вигляді растрових зображень, щоб їх можна було швидко намалювати та перемалювати (для анімації).

Моя проблема полягає в тому, що незалежно від того, як я намагаюся масштабувати свої зображення (будь то через matrix.postScale, matrix.preScale або функцію createScaledBitmap), вони завжди виходять нерівними та розмитими. Я знаю, що це масштабування, що спричиняє проблему, оскільки зображення виглядають ідеально, коли малюються без зміни розміру.

Я пропрацював кожне рішення, описане в цих двох потоках:
якість зображень, змінених під час виконання
проблем якості, під час зміни розміру зображення під час виконання

але досі нікуди не дійшли.

Я зберігаю свої растрові зображення (у хеш-мапі) з таким кодом:

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

і намалюйте їх цим методом (у класі Card):

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

Будь-яке розуміння буде вдячне. Дякую


Коли ви говорите "Цей код абсолютно не впливає", я припускаю, що ви встановлюєте параметр для setAntiAliasістина (і це все одно не має значення)?
Фредлі

Це правильно, я експериментував з true / false для всіх цих методів, і жоден з них не робить ніякої різниці
Cbas

Це питання було згадано в Meta SE
Лінні

Відповіді:


45

У мене були розмиті зображення на низькій роздільній здатності екрана, поки я не вимкнув масштабування при завантаженні растрових зображень із ресурсів:

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

Це спрацювало чудово! Кришталево чисті зображення, відсутність розмитості. Дякую .. Я виявив, що мені все ще потрібно використовувати метод WarrenFaith: фільтрування зображення позбавляє нерівності, а встановлення параметра inScaled на false - позбавляє розмитості. Дякуємо за допомогу всім!
Cbas

3
Чудові речі. Просто примітка, що a.getResources () можна скоротити до getResources (), а шлях є R.drawable.id бітової карти.
Shadoath

Це чудово працювало. Я прокоментував цей рядок o2.inSampleSize = масштаб; і додав цей рядок відповідно до вашого підсумку (o2.inScaled = false;). Подяка. Але я все-таки встановлював значення шкали як 1. Тоді чому проблема сталася, будь ласка, уточніть.
Сагар Деванга

88

Використовуйте createScaledBitmap, щоб ваше зображення виглядало дуже погано. Я зустрів цю проблему і вирішив її. Нижче код вирішить проблему:

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;

    }

Чудово! Ти геній! Щиро дякую: D
aberaud

6
Це рішення ідеальне (воно єдине, що працює для мене), однак ви можете позбутися middleXі middleY: просто поставте 0 і вкажітьcanvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG));
aberaud

4
Раніше я малював свої растрові зображення іншою фарбою, ніж Paint.FILTER_BITMAP_FLAGS. Перехід на Paint.FILTER_BITMAP_FLAG кардинально покращив мої результати! Дякую!
Дрор

Результати значно покращуються, але іноді метод не працює, коли я намагався використати цей метод відразу після того, як було знято зображення з камери ..
AnkitRox

4
Вибачте, але це рішення не працює в моєму випадку.
sumeet

34

createScaledBitmapмає прапор, де ви можете встановити, чи слід відфільтровувати масштабоване зображення чи ні. Цей прапор покращує якість растрового зображення ...


1
Ого, це набагато покращило! Дякую, я не можу повірити, що ніхто не згадав про це в інших нитках ... Це забрало всі гострі краї, але зображення все ще дуже розмиті. Будь-які ідеї щодо того, як позбутися розмитості?
Cbas

11

використовувати як

mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); 

Paint.FILTER_BITMAP_FLAG це робота для мене


1
Не знаю, чому це було проти, це було рішення для мене.
Стефан Матіс,

2
Дякую, це врятувало мені день! Я використав його як аргумент для методу
canvas.drawBitmap

8

Я припускаю, що ви пишете код для версії Android нижче 3.2 (рівень API <12), оскільки з тих пір поведінка методів

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

змінилося.

На старих платформах (рівень API <12) методи BitmapFactory.decodeFile (..) намагаються повернути Bitmap із конфігурацією RGB_565 за замовчуванням, якщо вони не можуть знайти альфа-версію, що знижує якість iamge. Це все ще нормально, оскільки ви можете застосувати растрове зображення ARGB_8888 за допомогою

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

Справжня проблема виникає, коли кожен піксель вашого зображення має альфа-значення 255 (тобто повністю непрозорий). У цьому випадку для прапора Bitmap 'hasAlpha' встановлено значення false, навіть якщо ваш Bitmap має конфігурацію ARGB_8888. Якби у вашому файлі * .png був хоча б один справжній прозорий піксель, для цього прапора було б встановлено значення true, і вам не довелося б ні про що турбуватися.

Отже, коли ви хочете створити масштабований растровий малюнок за допомогою

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

метод перевіряє, чи встановлено для прапора 'hasAlpha значення true чи false, а у вашому випадку - false, що призводить до отримання масштабованого растрового зображення, яке автоматично перетворюється у формат RGB_565.

Тому на рівні API> = 12 існує загальнодоступний метод, який називається

public void setHasAlpha (boolean hasAlpha);

який вирішив би це питання. Поки це було лише поясненням проблеми. Я провів кілька досліджень і помітив, що метод setHasAlpha існує вже давно і є загальнодоступним, але прихований (анотація @hide). Ось як це визначається на 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);
}

Зараз ось моя пропозиція щодо рішення. Він не передбачає копіювання растрових даних:

  1. Перевіряється під час виконання за допомогою java.lang.Reflect, чи поточна реалізація Bitmap має загальнодоступний метод 'setHasAplha'. (Згідно з моїми тестами, він чудово працює з API 3 рівня, і я не тестував нижчих версій, оскільки JNI не працював). У вас можуть виникнути проблеми, якщо виробник явно зробив його приватним, захистив або видалив.

  2. Викличте метод 'setHasAlpha' для даного об'єкта Bitmap за допомогою JNI. Це чудово працює, навіть для приватних методів або полів. Офіційно JNI не перевіряє, порушуєте Ви правила контролю доступу чи ні. Джерело: http://java.sun.com/docs/books/jni/html/pitfalls.html (10.9) Це дає нам велику силу, якою слід користуватися з розумом. Я б не намагався модифікувати остаточне поле, навіть якщо це спрацювало (лише для прикладу). І зауважте, це лише обхідний шлях ...

Ось моя реалізація всіх необхідних методів:

ЧАСТИНА ЯВА:

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

Завантажте свою бібліотеку та оголосіть власний метод:

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

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

Власний розділ (папка '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
}

Це воно. Ми закінчили. Я опублікував цілий код для копіювання та вставлення. Насправді код не такий великий, але всі ці параноїчні перевірки помилок роблять його набагато більшим. Сподіваюся, це може бути корисним для когось.


Чи можете ви, будь ласка, додати вищезазначені коди до зразкового проекту для Android у GitHub? Ваш підхід цікавий.
SMMousavi

Вибачте, але у мене вже немає налаштувань для власної розробки.
Іво

8

Хороший алгоритм зменшення масштабу (не такий, як найближчий сусід, тому не додається пікселізація) складається лише з 2 кроків (плюс обчислення точного прямого для обрізання вхідних / вихідних зображень):

  1. зменшення масштабу за допомогою BitmapFactory.Options :: inSampleSize -> BitmapFactory.decodeResource () якомога ближче до потрібної роздільної здатності, але не менше її
  2. дістатися до точного дозволу, трохи зменшивши масштаб, використовуючи Canvas :: drawBitmap ()

Ось детальне пояснення того, як SonyMobile вирішив це завдання: https://web.archive.org/web/20171011183652/http://developer.sonymobile.com/2011/06/27/how-to-scale-images-for- ваш-андроїд-додаток /

Ось вихідний код використання масштабу SonyMobile: https://web.archive.org/web/20170105181810/http://developer.sonymobile.com:80/downloads/code-example-module/image-scaling-code- приклад для android /


4

Ви ніколи не отримаєте ідеального результату, якщо збільшите свої растрові зображення.

Ви повинні розпочати з найвищою роздільною здатністю, яку вам потрібно, і зменшити масштаб.

Під час масштабування растрового зображення масштабування не може здогадатися, які відсутні точки між кожною існуючою точкою, тому воно або продублює сусід (=> острий), або обчислює середнє значення між сусідами (=> розмито).


Це має цілковитий сенс, хоча, використовуючи методи, описані вище, мені вдалося отримати дуже чітке зображення - настільки чітке, що я не бачу різниці між реальним зображенням і масштабованим. Я впевнений, що він не ідеальний, але напевно схожий на це (і я використовую одну з найновіших моделей Android для тестування). Дійсна точка. Мені точно доведеться врахувати, що якщо я хочу випустити цю гру для планшета
Cbas


1

Якщо ви хочете отримати якісний результат, використовуйте бібліотеку [RapidDecoder] [1]. Це просто наступним чином:

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

Не забудьте використовувати вбудований декодер, якщо ви хочете зменшити масштаб менше 50% і отримати результат HQ. Я протестував це на API 8.


1

Виникла ця проблема під час оновлення Android Target Framework з Android 8.1 до Android 9 і проявилася на моєму ImageEntryRenderer. Сподіваюся, це допомагає

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

Примітка: Я розробляю під фреймворком 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;

    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.