Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does "Bitmap.createScaledBitmap" convert an 32 bit image into 24 bit?

Tags:

android

bitmap

In my app I load an image as 32 bit (ARGB_8888) this way:

 Bitmap.Config mBitmapConfig;
 mBitmapConfig = Bitmap.Config.ARGB_8888; 
 BitmapFactory.Options options = new BitmapFactory.Options();
 options.inPreferredConfig = mBitmapConfig;
 mBitmap = BitmapFactory.decodeFile(SourceFileName, options);

Then scale:

mBitmap = Bitmap.createScaledBitmap(mBitmap, iW, iH, true); 

If I use for scaling the same Width and Height of the original bitmap, it is 1/2 of the size in megabytes (I'm watching the heap size). Changing the value "ARGB_8888" to "RGB_565" (24 bit) gives the same size in megabytes after scaling.

Can someone explain this phenomenon and may be give me an advice, how to scale bitmaps in 32 bit color space? Thanks!

like image 580
pbu Avatar asked Jun 08 '11 12:06

pbu


4 Answers

I looked up the method createScaledBitmap in the source for the Bitmap class (Link):

public static Bitmap createScaledBitmap(Bitmap src, int dstWidth,
        int dstHeight, boolean filter) {
    Matrix m;
    synchronized (Bitmap.class) {
        // small pool of just 1 matrix
        m = sScaleMatrix;
        sScaleMatrix = null;
    }

    if (m == null) {
        m = new Matrix();
    }

    final int width = src.getWidth();
    final int height = src.getHeight();
    final float sx = dstWidth  / (float)width;
    final float sy = dstHeight / (float)height;
    m.setScale(sx, sy);
    Bitmap b = Bitmap.createBitmap(src, 0, 0, width, height, m, filter);

    synchronized (Bitmap.class) {
        // do we need to check for null? why not just assign everytime?
        if (sScaleMatrix == null) {
            sScaleMatrix = m;
        }
    }

    return b;
}

And the call to createBitmap() should return your unchanged source bitmap due to this check in the method body:

    if (!source.isMutable() && x == 0 && y == 0 && width == source.getWidth() &&
            height == source.getHeight() && (m == null || m.isIdentity())) {
        return source;
    }

Looking at just this it would seem that your original bitmap is returned, But, if your bitmap happens to be mutable, you effectively skip this check and end up here:

    if (m == null || m.isIdentity()) {
        bitmap = createBitmap(neww, newh,
                source.hasAlpha() ? Config.ARGB_8888 : Config.RGB_565);
        paint = null;   // not needed
    }

As you are not performing any scaling, your matrix will be the identity matrix, and the condition is satisfied. The bitmap created is, as you can see, dependent on the alpha in the source bitmap. If no alpha is present, you end up with a result bitmap with the RGB_565 format rather than the ARGB_8888.

So, to scale and preserve the 32-bit format, your bitmap should either be immutable or use an Alpha channel.

like image 105
Jave Avatar answered Oct 22 '22 22:10

Jave


Color Banding Solved ooooooooooyyyyyyyeaaaaaaaaaa

I solved color banding in two phases

1) * when we use the BitmapFactory to decode resources it decodes the resource in RGB565 which shows color banding, instead of using ARGB_8888, so i used BitmapFactory.Options for setting the decode options to ARGB_8888

second problem was whenever i scaled the bitmap it again got banded

2) This was the tough part and took a lot of searching and finally worked * the method Bitmap.createScaledBitmap for scaling bitmaps also reduced the images to RGB565 format after scaling i got banded images(the old method for solving this was using at least one transparent pixel in a png but no other format like jpg or bmp worked)so here i created a method CreateScaledBitmap to scale the bitmap with the original bitmaps configurations in the resulting scale bitmap(actually i copied the method from a post by logicnet.dk and translated in java)

    BitmapFactory.Options myOptions = new BitmapFactory.Options();
    myOptions.inDither = true;
    myOptions.inScaled = false;
    myOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//important
    //myOptions.inDither = false;
    myOptions.inPurgeable = true;
    Bitmap tempImage =  
    BitmapFactory.decodeResource(getResources(),R.drawable.defaultart, myOptions);//important

    //this is important part new scale method created by someone else
    tempImage = CreateScaledBitmap(tempImage,300,300,false);

    ImageView v = (ImageView)findViewById(R.id.imageView1);
    v.setImageBitmap(tempImage);

// the function

public static Bitmap CreateScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter)
{
    Matrix m = new Matrix();
    m.setScale(dstWidth  / (float)src.getWidth(), dstHeight / (float)src.getHeight());
    Bitmap result = Bitmap.createBitmap(dstWidth, dstHeight, src.getConfig());
    Canvas canvas = new Canvas(result);

        Paint paint = new Paint();
        paint.setFilterBitmap(filter);
        canvas.drawBitmap(src, m, paint);

    return result;

}

Please correct me if i am wrong. Also comment if it worked for you.

I am so happy i solved it, Hope it works for you.

like image 42
Diljeet Avatar answered Oct 22 '22 20:10

Diljeet


It's easy to create your own version that keeps the pixel format of the source:

public static Bitmap CreateScaledBitmap(Bitmap src, int dstWidth, int dstHeight, bool filter)
{
    var m = new Matrix();
    m.SetScale(dstWidth  / (float)src.Width, dstHeight / (float)src.Height);
    var result = Bitmap.CreateBitmap(dstWidth, dstHeight, src.GetConfig());
    using (var canvas = new Canvas(result))
    {
        var paint = new Paint();
        paint.FilterBitmap = filter;
        canvas.DrawBitmap(src, m, paint);
    }
    return result;
}

(Code is for Monodroid, but it should be easy to translate to Java)

like image 1
logicnet.dk Avatar answered Oct 22 '22 20:10

logicnet.dk


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.

like image 1
Ivo Avatar answered Oct 22 '22 21:10

Ivo