Bad image quality after resizing/scaling bitmap
Asked Answered
B

11

79

I'm writing a card game and need my cards to be different sizes in different circumstances. I am storing my images as bitmaps so that they can be quickly drawn and redrawn (for animation).

My problem is that no matter how I try and scale my images (whether through a matrix.postScale, a matrix.preScale, or a createScaledBitmap function) they always come out pixelated and blurry. I know that its the scaling thats causing the problem because the images look perfect when drawn without resizing.

I have worked through every solution described in these two threads:
android quality of the images resized in runtime
quality problems when resizing an image at runtime

but still haven't gotten anywhere.

I store my bitmaps (into a hashmap) with this code:

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

and draw them with this method (in a Card class):

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

Any insight would be greatly appreciated. Thanks

Blender answered 27/1, 2011 at 20:29 Comment(3)
When you say "This code makes absolutely no difference", I assume you are setting the parameter for setAntiAlias to true (and it still makes no difference)?Relay
Thats correct, I've experimented with true/false for all of those methods and none of them make any differenceBlender
This question has been mentioned on Meta SECross
S
48

I had blury images on low screen resolutions until I disabled scaling on bitmap load from resources:

Options options = new BitmapFactory.Options();
    options.inScaled = false;
    Bitmap source = BitmapFactory.decodeResource(a.getResources(), path, options);
Smasher answered 27/1, 2011 at 21:55 Comment(3)
That worked perfectly! Crystal clear images, no blurriness. Thanks.. I've found that I still need to use WarrenFaith's method as well: filtering the image gets rid of the jaggedness, and setting the inScaled option to false gets rid of the blurriness. Thanks for the help everyone!Blender
Great stuff. Just a note that a.getResources() can be shortened to just getResources() and path is the R.drawable.id of the bitmap.Stirk
That worked great. I commented this line o2.inSampleSize=scale; and added this line as per your sugesstion ( o2.inScaled = false; ). Thanx . But I was anyways setting the scale value as 1. Then why did the problem happen could you please elaborate.Howes
C
91

Use createScaledBitmap will make your image looks very bad. I've met this problem and I've resolved it. Below code will fix the 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;

    }
Caruthers answered 19/9, 2011 at 9:9 Comment(6)
This solution is perfect (it's the only one working for me), however you can get rid of middleX and middleY : just put 0 and state canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG));Couple
I used to draw my bitmaps with a different paint than Paint.FILTER_BITMAP_FLAGS. Changing to Paint.FILTER_BITMAP_FLAG has drastically improved my results! Thanks!Brimstone
Results are quite improved, but sometimes method doesn't work when I tried to use this method just after camera captured image..Rhapsodist
Sorry to say, but this solution is not working in my case.Systaltic
i tried this solution, quality is good, but 1/4 part of bitmap is returned.Bornu
Still getting a blurred image , Quality is still badRestriction
S
48

I had blury images on low screen resolutions until I disabled scaling on bitmap load from resources:

Options options = new BitmapFactory.Options();
    options.inScaled = false;
    Bitmap source = BitmapFactory.decodeResource(a.getResources(), path, options);
Smasher answered 27/1, 2011 at 21:55 Comment(3)
That worked perfectly! Crystal clear images, no blurriness. Thanks.. I've found that I still need to use WarrenFaith's method as well: filtering the image gets rid of the jaggedness, and setting the inScaled option to false gets rid of the blurriness. Thanks for the help everyone!Blender
Great stuff. Just a note that a.getResources() can be shortened to just getResources() and path is the R.drawable.id of the bitmap.Stirk
That worked great. I commented this line o2.inSampleSize=scale; and added this line as per your sugesstion ( o2.inScaled = false; ). Thanx . But I was anyways setting the scale value as 1. Then why did the problem happen could you please elaborate.Howes
C
36

createScaledBitmap has a flag where you can set if the scaled image should be filtered or not. That flag improves the quality of the bitmap...

Certified answered 27/1, 2011 at 20:32 Comment(1)
Wow, that improved it a lot! Thanks, I can't believe no one mentioned that on the other threads... It took away all of the sharp edges, but the images are still very blurry. Any ideas on how to get rid of the blur?Blender
L
12

use as

mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); 

Paint.FILTER_BITMAP_FLAG is work for me

Loading answered 7/8, 2012 at 7:18 Comment(2)
Don't know why this has been downvoted, it was the solution for me.Cryptic
Thanks, this saved my day! I used it as argument for canvas.drawBitmap methodAurelio
M
8

I assume you are writing code for a version of Android lower than 3.2 (API level < 12), because since then the behavior of the methods

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

has changed.

On older platforms (API level < 12) the BitmapFactory.decodeFile(..) methods try to return a Bitmap with RGB_565 config by default, if they can't find any alpha, which lowers the quality of an iamge. This is still ok, because you can enforce an ARGB_8888 bitmap using

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

The real problem comes when each pixel of your image has an alpha value of 255 (i.e. completely opaque). In that case the Bitmap's flag 'hasAlpha' is set to false, even though your Bitmap has ARGB_8888 config. If your *.png-file had at least one real transparent pixel, this flag would have been set to true and you wouldn't have to worry about anything.

So when you want to create a scaled Bitmap using

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

the method checks whether the 'hasAlpha' flag is set to true or false, and in your case it is set to false, which results in obtaining a scaled Bitmap, which was automatically converted to the RGB_565 format.

Therefore on API level >= 12 there is a public method called

public void setHasAlpha (boolean hasAlpha);

which would have solved this issue. So far this was just an explanation of the problem. I did some research and noticed that the setHasAlpha method has existed for a long time and it's public, but has been hidden (@hide annotation). Here is how it is defined on 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);
}

Now here is my solution proposal. It does not involve any copying of bitmap data:

  1. Checked at runtime using java.lang.Reflect if the current Bitmap implementation has a public 'setHasAplha' method. (According to my tests it works perfectly since API level 3, and i haven't tested lower versions, because JNI wouldn't work). You may have problems if a manufacturer has explicitly made it private, protected or deleted it.

  2. Call the 'setHasAlpha' method for a given Bitmap object using JNI. This works perfectly, even for private methods or fields. It is official that JNI does not check whether you are violating the access control rules or not. Source: http://java.sun.com/docs/books/jni/html/pitfalls.html (10.9) This gives us great power, which should be used wisely. I wouldn't try modifying a final field, even if it would work (just to give an example). And please note this is just a workaround...

Here is my implementation of all necessary methods:

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

Load your lib and declare the native method:

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

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

Native section ('jni' folder)

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
}

That's it. We are done. I've posted the whole code for copy-and-paste purposes. The actual code isn't that big, but making all these paranoid error checks makes it a lot bigger. I hope this could be helpful to anyone.

Mayweed answered 30/8, 2012 at 17:12 Comment(2)
Can you please add above codes in a sample android project in GitHub? Your approach is interesting.Lachrymator
Sorry, but I don't have the setup for native development anymore.Mayweed
G
8

Good downscaling algorithm (not nearest neighbor like, so no pixelation is added) consists of just 2 steps (plus calculation of the exact Rect for input/output images crop):

  1. downscale using BitmapFactory.Options::inSampleSize -> BitmapFactory.decodeResource() as close as possible to the resolution that you need but not less than it
  2. get to the exact resolution by downscaling a little bit using Canvas::drawBitmap()

Here is detailed explanation how SonyMobile resolved this task: https://web.archive.org/web/20171011183652/http://developer.sonymobile.com/2011/06/27/how-to-scale-images-for-your-android-application/

Here is the source code of SonyMobile scale utils: https://web.archive.org/web/20170105181810/http://developer.sonymobile.com:80/downloads/code-example-module/image-scaling-code-example-for-android/

Gametophore answered 21/4, 2014 at 2:3 Comment(0)
P
4

You will never have a perfect result if you scale your bitmaps up.

You should start with the highest resolution you need and scale down.

When scaling a bitmap up, the scaling can't guess what are the missing points between each existing point, so it either duplicates a neighbour (=> edgy) or calculates a mean value between neighbours (=> blurry).

Proven answered 27/1, 2011 at 23:14 Comment(1)
That makes total sense, although using the methods described above, I have been able to get a very clear image - so clear that I can't see any difference between the real image and the scaled up one. I'm sure its not perfect, but it definitely looks like it (and I'm using one of the newest Android models to test it). Valid point though. I'll definitely have to consider that if I want to release this game for a tabletBlender
R
4

I just used flag filter=true in bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true); for blur.

Ruelle answered 5/10, 2013 at 18:41 Comment(0)
L
1

If you want high quality result, so use [RapidDecoder][1] library. It is simple as follow:

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

Don't forget to use builtin decoder if you want to scale down less than 50% and a HQ result. I tested it on API 8.

Lachrymator answered 25/8, 2016 at 11:50 Comment(0)
S
1

Had this issue upon updating Android Target Framework from Android 8.1 to Android 9 and manifested on my ImageEntryRenderer. Hope this helps

    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: I am developing under Xamarin 3.4.0.10 framework

Swanky answered 25/6, 2019 at 2:31 Comment(0)
S
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;

    }
Scincoid answered 16/9, 2017 at 15:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.