Quality problems when resizing an image at runtime

34,429

Solution 1

After experimenting I have finally found a way to do this with good quality results. I'll write this up for anyone that might find this answer helpful in the future.

To solve the first problem, the artifacts and weird dithering introduced into the images, you need to insure your image stays as a 32-bit ARGB_8888 image. Using the code in my question, you can simply add this line to the options before the second decode.

options.inPreferredConfig = Bitmap.Config.ARGB_8888;

After adding that, the artifacts were gone but edges throughout the images came through jagged instead of crisp. After some more experimentation I discovered that resizing the bitmap using a Matrix instead of Bitmap.createScaledBitmap produced much crisper results.

With those two solutions, the images are now resizing perfectly. Below is the code I am using in case it benefits someone else coming across this problem.

// Get the source image's dimensions
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(STRING_PATH_TO_FILE, options);

int srcWidth = options.outWidth;
int srcHeight = options.outHeight;

// Only scale if the source is big enough. This code is just trying to fit a image into a certain width.
if(desiredWidth > srcWidth)
    desiredWidth = srcWidth;



// Calculate the correct inSampleSize/scale value. This helps reduce memory use. It should be a power of 2
// from: https://stackoverflow.com/questions/477572/android-strange-out-of-memory-issue/823966#823966
int inSampleSize = 1;
while(srcWidth / 2 > desiredWidth){
    srcWidth /= 2;
    srcHeight /= 2;
    inSampleSize *= 2;
}

float desiredScale = (float) desiredWidth / srcWidth;

// Decode with inSampleSize
options.inJustDecodeBounds = false;
options.inDither = false;
options.inSampleSize = inSampleSize;
options.inScaled = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap sampledSrcBitmap = BitmapFactory.decodeFile(STRING_PATH_TO_FILE, options);

// Resize
Matrix matrix = new Matrix();
matrix.postScale(desiredScale, desiredScale);
Bitmap scaledBitmap = Bitmap.createBitmap(sampledSrcBitmap, 0, 0, sampledSrcBitmap.getWidth(), sampledSrcBitmap.getHeight(), matrix, true);
sampledSrcBitmap = null;

// Save
FileOutputStream out = new FileOutputStream(NEW_FILE_PATH);
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
scaledBitmap = null;

EDIT: After continual work on this I have found that the images still aren't 100% perfect. I'll make an update if I can improve it.

Update: After revisting this, I found this question on SO and there was an answer that mentioned the inScaled option. This helped with the quality as well so I added updated the answer above to include it. I also now null the bitmaps after they are done being used.

Also, as a side note, if you are using these images in a WebView, make sure you take this post into consideration.

Note: you should also add a check to make sure the width and height are valid numbers (not -1). If they are, it will cause the inSampleSize loop to become infinite.

Solution 2

In my situation I am drawing the image to the screen. Here's what I did to get my images to look correct (a combination of littleFluffyKitty's answer, plus a few other things).

For my options when I actually load the image (using decodeResource) I set the following values:

    options.inScaled = false;
    options.inDither = false;
    options.inPreferredConfig = Bitmap.Config.ARGB_8888;

When I actually draw the image, I set up my paint object like this:

    Paint paint = new Paint();
    paint.setAntiAlias(true);
    paint.setFilterBitmap(true);
    paint.setDither(true);

Hopefully someone else finds that useful too. I wish there were just options for "Yes, let my resized images look like garbage" and "No, please don't force my users to gouge their eyes out with spoons" instead of all the myriad of different options. I know they want to give us lots of control, but maybe some helper methods for common settings could be useful.

Solution 3

I created simple library based on littleFluffyKitty answer which does resize and does some other things like crop and rotation so please free to use it and improve it - Android-ImageResizer.

Solution 4

"However, I have no idea how to make sure the Bitmap stays as 32 bits through the whole process."

I wanted to post an alternative solution, which takes care of keeping the ARGB_8888 config untouched. NOTE: This code only decodes bitmaps and needs to be extended, so you could store a Bitmap.

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.

Solution 5

onScreenResults = Bitmap.createScaledBitmap(tempBitmap, scaledOSRW, scaledOSRH, true);  <----

setting the filter to true worked for me.

Share:
34,429
cottonBallPaws
Author by

cottonBallPaws

Updated on August 10, 2020

Comments

  • cottonBallPaws
    cottonBallPaws almost 4 years

    I have a image file on the disk and I am resizing the file and saving it back to disk as a new image file. For the sake of this question, I am not bringing them into memory in order to display them on the screen, only to resize them and resave them. This all works just fine. However, the scaled images have artifacts on them like shown here: android: quality of the images resized in runtime

    They are saved with this distortion, as I can pull them off the disk and look at them on my computer and they still have the same issue.

    I am using code similar to this Strange out of memory issue while loading an image to a Bitmap object to decode the bitmap into memory:

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(imageFilePathString, options);
    
    int srcWidth = options.outWidth;
    int srcHeight = options.outHeight;
    int scale = 1;
    
    while(srcWidth / 2 > desiredWidth){
       srcWidth /= 2;
       srcHeight /= 2;
       scale *= 2;
    }
    
    options.inJustDecodeBounds = false;
    options.inDither = false;
    options.inSampleSize = scale;
    Bitmap sampledSrcBitmap = BitmapFactory.decodeFile(imageFilePathString, options);
    

    Then I am doing the actual scaling with:

    Bitmap scaledBitmap = Bitmap.createScaledBitmap(sampledSrcBitmap, desiredWidth, desiredHeight, false);
    

    Lastly, the new resized image is saved to disk with:

    FileOutputStream out = new FileOutputStream(newFilePathString);
    scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
    

    Then, as I mentioned, if I pull that file off the disk and look at it, it has that quality issue linked above and looks terrible. If I skip the createScaledBitmap and just save the sampledSrcBitmap right back to disk there is no problem, it seems to only happen if the size changes.

    I have tried, as you can see in the code, setting inDither to false as mentioned here http://groups.google.com/group/android-developers/browse_thread/thread/8b1abdbe881f9f71 and as mentioned in the very first linked post above. That didn't change anything. Also, in the first post I linked, Romain Guy said:

    Instead of resizing at drawing time (which is going to be very costly), try to resize in an offscreen bitmap and make sure that Bitmap is 32 bits (ARGB888).

    However, I have no idea how to make sure the Bitmap stays as 32 bits through the whole process.

    I have also read a couple other articles such as this http://android.nakatome.net/2010/04/bitmap-basics.html but they all seemed to be addressing drawing and displaying the Bitmap, I just want to resize it and save it back to disk without this quality problem.

    Thanks much