Плохое качество изображения после изменения размера / масштабирования растрового изображения - PullRequest
74 голосов
/ 27 января 2011

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

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

Я работал над каждым решением, описанным в этих двух потоках:
качество андроид изображений, измененных во время выполнения
проблемы с качеством при изменении размера изображения во время выполнения

но до сих пор не получил нигде.

Я храню свои растровые изображения (в hashmap) с этим кодом:

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

Любое понимание будет с благодарностью. Спасибо

Ответы [ 11 ]

79 голосов
/ 19 сентября 2011

Использование 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;

    }
45 голосов
/ 28 января 2011

У меня были размытые изображения на низких разрешениях экрана, пока я не отключил масштабирование при загрузке растрового изображения из ресурсов:

Options options = new BitmapFactory.Options();
    options.inScaled = false;
    Bitmap source = BitmapFactory.decodeResource(a.getResources(), path, options);
33 голосов
/ 27 января 2011

createScaledBitmap имеет флаг, где вы можете установить, следует ли фильтровать масштабированное изображение или нет. Этот флаг улучшает качество растрового изображения ...

10 голосов
/ 07 августа 2012

использовать как

mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); 

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

8 голосов
/ 30 августа 2012

Я предполагаю, что вы пишете код для версии Android ниже 3.2 (уровень API <12), потому что с тех пор поведение методов </p>

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

изменилось.

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

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

. Настоящая проблема возникает, когда каждый пиксель вашего изображения имеет значение альфа 255 (то есть полностью непрозрачный).В этом случае флаг растрового изображения hasAlpha имеет значение false, даже если у вашего растрового изображения есть конфигурация 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, если текущая реализация растрового изображения имеет открытый метод setHasAplha.(Согласно моим тестам, он отлично работает, начиная с уровня API 3, и я не тестировал более низкие версии, потому что JNI не будет работать).У вас могут возникнуть проблемы, если производитель явно сделал его закрытым, защитил или удалил его.

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

Вот моя реализация всех необходимых методов:

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

Загрузите вашу библиотеку и объявите собственный метод:

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
}

Вот и все.Мы сделали.Я разместил весь код в целях копирования и вставки.Фактический код не такой большой, но выполнение всех этих параноидальных проверок ошибок делает его намного больше.Я надеюсь, что это может быть полезно для всех.

7 голосов
/ 21 апреля 2014

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

  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-your-android-application/

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

4 голосов
/ 28 января 2011

У вас никогда не будет идеального результата, если вы увеличите свои растровые изображения.

Вы должны начать с самого высокого необходимого разрешения и уменьшить его.

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

3 голосов
/ 05 октября 2013

Я только что использовал флаг filter=true в bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true); для размытия.

1 голос
/ 25 августа 2016

Если вы хотите получить высококачественный результат, используйте библиотеку [RapidDecoder] [1]. Это просто, как следует:

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

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

0 голосов
/ 25 июня 2019

Возникла эта проблема при обновлении 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

...