Quality issues when resizing images at runtime

I have an image file on disk, and I resize the file and save it back to disk as a new image file. For the sake of this question, I do not bring them into memory in order to display them on the screen, only to resize them and save them. It all works great. However, scaled images have artifacts on them, as shown here: android: quality of images altered at runtime

They are saved with this distortion, since I can remove them from the disk and look at them on my computer, and they still have the same problem.

I use code like this A strange memory issue when loading an image into a Bitmap object to decode a bitmap in 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 do the actual scaling with:

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

Finally, the new modified image is saved to disk using:

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

Then, as I already mentioned, if I pulled this file out of the disk and looked at it, it has a quality problem related above and it looks awful. If I skip createScaledBitmap and just save sampledSrcBitmap directly to disk, no problem, it seems to happen only when resizing.

I tried, as you can see in the code, to set the inDither value to false, as indicated here http://groups.google.com/group/android-developers/browse_thread/thread/8b1abdbe881f9f71 and, as mentioned in the very first related post above . This has not changed anything. Also, in the first post I linked, Romain Guy said:

Instead of resizing while drawing (which will be very expensive), try resizing in the bitmap off-screen and make sure the bitmap is 32 bits (ARGB888).

However, I do not know how to make sure that the bitmap remains 32 bits throughout the process.

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

Thank you so much

+26
android image resize bitmap
Nov 20 '10 at 8:11
source share
7 answers

After the experiment, I finally found a way to do this with good quality results. I will write this for anyone who may find this answer useful in the future.

To solve the first problem, artifacts and strange anti-aliasing introduced into the images, you need to insure your image as a 32-bit image ARGB_8888. Using the code in my question, you can simply add this line to the parameters before the second decoding.

 options.inPreferredConfig = Bitmap.Config.ARGB_8888; 

Adding that the artifacts disappeared, but the edges in all the images appeared through the jagged, not clear. After several experiments, I found that resizing a bitmap using a matrix instead of Bitmap.createScaledBitmap gives much clearer results.

Thanks to these two solutions, the images are now changing perfectly. Below is the code I use if it benefits someone else who is facing this problem.

 // Get the source image 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 constantly working on this, I found that the images are still not 100% perfect. I will make an update if I can improve it.

Update: After you confirmed this, I found this question in SO format and there was an answer that mentioned the inScaled option. It also helped quality, so I added the updated answer above to include it. I also now zero bitmaps after they have been made.

Also, as a side note, if you use these images in a WebView, make sure you take this post into account.

Note. You should also add a check to make sure that the width and height are real numbers (not -1). If they are, this will cause the inSampleSize loop to become infinite.

+53
Nov 22 2018-10-21
source share

In my situation, I draw an image on the screen. Here's what I did to make my images look right (a combination of littleFluffyKitty's answer, plus a few other things).

For my parameters, 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 an image, I set up my drawing object as follows:

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

Hope someone else finds this helpful. I am sorry that there were no options for “Yes, let my modified images look like garbage” and “No, please do not force my users to knock their eyes out with spoons,” and not all of the many different options. I know that they want to give us a lot of control, but maybe some helper methods for general settings can be useful.

+8
Mar 18 '11 at 17:35
source share

I created a simple library based on littleFluffyKitty answer, which resizes and does some other things, such as cropping and rotation, so please use it for free and improve it - Android-ImageResizer .

+3
Nov 24 '12 at 19:18
source share

"However, I don’t know how to make sure that the bitmap remains 32 bits through the whole process."

I wanted to publish an alternative solution that would take care to keep the configuration of ARGB_8888 intact. NOTE. This code only decodes bitmaps and needs to be expanded, so you can save the bitmap.

I assume that you are writing code for the Android version below 3.2 (API level <12), since 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 Bitmap with the RGB_565 configuration by default if they cannot find alpha, which reduces the quality of iamge. This is still ok because you can use the ARGB_8888 bitmap using

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

The real problem arises when every pixel in your image has an alpha value of 255 (i.e. completely opaque). In this case, the Bitmap flag 'hasAlpha' is false, even if your bitmap has the configuration ARGB_8888. If your * .png file had at least one real transparent pixel, this flag would be set to true, and you should not worry about anything.

So, if you want to create a scaled bitmap using

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

the method checks if the hasAlpha flag is set to true or false, and in your case it is set to false, which results in a scaled bitmap that is automatically converted to RGB_565 format.

Therefore, at the API level> = 12, there is a public method called

 public void setHasAlpha (boolean hasAlpha); 

which would solve this problem. So far, this has been just an explanation of the problem. I did some research and noticed that the setHasAlpha method has existed for a long time, and it is public, but it was 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 (eg 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. This is not related to copying raster data:

  • Checked at runtime using java.lang.Reflect if the current Bitmap Implementation has a public setHasAplha method. (According to my tests, it works fine from API level 3, and I have not tested lower versions because JNI did not work). You may have problems if the manufacturer has explicitly made this confidential, protected or deleted it.

  • Call the setHasAlpha method for this Bitmap using the JNI. This works great even for private methods or fields. Officially, JNI does not check whether you violate access control rules or not. Source: http://java.sun.com/docs/books/jni/html/pitfalls.html (10.9) This gives us great power that should be wisely used. I would not modify the last field, even if it worked (just to give an example). And note that this is just a workaround ...

Here is my implementation of all the 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; } 

Download your lib and declare your own 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 } 

What is it. We have done. I posted all the code for copy and paste. The actual code is not that big, but all these checks for paranoid errors make it so much more. Hope this can be helpful to everyone.

+2
Aug 30 2018-12-12T00:
source share
 onScreenResults = Bitmap.createScaledBitmap(tempBitmap, scaledOSRW, scaledOSRH, true); <---- 

setting the filter to true for me.

+1
Aug 09 '15 at 22:10
source share

So, createScaledBitmap and createBitmap (with a matrix that scales) on an immutable bitmap (for example, when decoding) will ignore the original Bitmap.Config and create a bitmap with Bitmap.Config.ARGB_565 if the original has no transparency (hasAlpha == false) . But he will not do this on a choppy bitmap. So, if your decoded bitmap is b:

 Bitmap temp = Bitmap.createBitmap(b.getWidth(), b.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(temp); canvas.drawBitmap(b, 0, 0, null); b.recycle(); 

Now you can resize the tempo, and it should save Bitmap.Config.ARGB_8888.

0
Oct 24
source share

Image scaling can also be achieved this way without loss of quality!

  //Bitmap bmp passed to method... ByteArrayOutputStream stream = new ByteArrayOutputStream(); bmp.compress(Bitmap.CompressFormat.JPEG, 100, stream); Image jpg = Image.getInstance(stream.toByteArray()); jpg.scalePercent(68); // or any other number of useful methods. 
0
Jan 10 '13 at 0:51
source share



All Articles