在运行时调整图像的质量问题
我在磁盘上有一个图像文件,我正在调整文件大小并将其保存为磁盘作为新的图像文件。 为了这个问题,我没有把它们带入记忆中,只是为了在屏幕上显示它们,只是调整它们并重新保存它们。 这一切工作得很好。 然而,缩放的图像有像这里显示的人工制品: android:在运行时resize的图像的质量
他们被保存了这个扭曲,因为我可以把他们从磁盘上拿出来,在我的电脑上看着他们,他们仍然有同样的问题。
我正在使用类似于此奇怪的内存不足问题的代码加载到Bitmap对象的图像解码到内存中的位图:
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);
然后我正在做的实际比例:
Bitmap scaledBitmap = Bitmap.createScaledBitmap(sampledSrcBitmap, desiredWidth, desiredHeight, false);
最后,新的resize的图像被保存到磁盘:
FileOutputStream out = new FileOutputStream(newFilePathString); scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
然后,正如我所提到的那样,如果我把这个文件从磁盘上取下来看看,那么上面就有这个质量问题,看起来很糟糕。 如果我跳过createScaledBitmap并将采样的SrcBitmap直接保存到磁盘上,没有任何问题,似乎只有在大小发生变化时才会发生。
我已经尝试过了,正如你在代码中看到的那样,在http://groups.google.com/group/android-developers/browse_thread/thread/8b1abdbe881f9f71中设置inDither为false,正如上面第一个链接的post中提到的。 这并没有改变任何东西。 另外,在我链接的第一篇文章中,罗曼·盖伊说:
而不是调整绘制时间(这将是非常昂贵的),尝试在离屏位图resize,并确保位图是32位(ARGB888)。
但是,我不知道如何确保位图在整个过程中保持32位。
我也读过一些其他的文章,例如http://android.nakatome.net/2010/04/bitmap-basics.html,但他们似乎都在处理绘图和显示位图,我只是想调整它,保存到磁盘没有这个质量问题。
非常感谢
经过试验,我终于find了一个方法来做到这一点,质量好的结果。 我会写这个给任何人可能会发现这个答案在未来的帮助。
为了解决第一个问题,图像中引入了伪像和奇怪的抖动,您需要确保图像保持为32位的ARGB_8888图像。 使用我的问题中的代码,你可以简单地将这一行添加到第二次解码之前的选项。
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
在添加之后,文物消失了,但是整个图像的边缘是通过锯齿而不是清脆的。 经过一些更多的实验后,我发现使用Matrix而不是Bitmap.createScaledBitmap来调整位图的大小会产生更为清晰的结果。
有了这两个解决scheme,图像现在可以完美resize。 下面是我使用的代码,以防止其他人遇到此问题。
// 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;
编辑:经过不断的工作,我发现图像仍然不是100%完美。 如果我可以改进的话,我会进行更新。
更新:修改后,我发现这个问题 ,所以有一个答案,提到inScaled选项。 这也有助于提高质量,所以我增加了更新上面的答案来包括它。 我现在也完成了使用后的位图。
此外,作为一个侧面说明,如果您在WebView中使用这些图像,请确保考虑到这个post。
注意:您还应该添加一个检查,以确保宽度和高度是有效的数字(不是-1)。 如果是的话,会导致inSampleSize循环变成无限的。
在我的情况下,我正在将图像绘制到屏幕上。 这是我做了什么让我的图像看起来是正确的(littleFluffyKitty的答案,加上一些其他的东西的组合)。
对于我实际加载图像时的选项(使用decodeResource),我设置了以下值:
options.inScaled = false; options.inDither = false; options.inPreferredConfig = Bitmap.Config.ARGB_8888;
当我实际绘制图像时,我设置了我的绘画对象,如下所示:
Paint paint = new Paint(); paint.setAntiAlias(true); paint.setFilterBitmap(true); paint.setDither(true);
希望别人也能find有用的东西。 我希望只有“是的,让我resize的图像看起来像垃圾”和“不,请不要强迫我的用户用勺子挖出他们的眼睛”,而不是所有的不同选项。 我知道他们想要给我们很多的控制权,但也许有些常用设置的帮手方法是有用的。
我创build了基于littleFluffyKitty
简单的库,resize,做一些其他的东西,如裁剪和旋转,所以请随意使用它,并改进它 – Android的ImageResizer 。
“但是,我不知道如何确保位图在整个过程中保持32位。”
我想发布一个替代解决scheme,它会保持ARGB_8888configuration不变。 注:此代码只解码位图,需要扩展,所以你可以存储一个位图。
我假设你正在编写Android版本低于3.2(API级别<12)的代码,因为从那时起这些方法的行为
BitmapFactory.decodeFile(pathToImage); BitmapFactory.decodeFile(pathToImage, opt); bitmapObject.createScaledBitmap(bitmap, desiredWidth, desiredHeight, false /*filter?*/);
已经改变。
在较旧的平台(API级别<12),BitmapFactory.decodeFile(..)方法默认情况下会尝试返回一个带有RGB_565configuration的位图,如果它们找不到任何alpha,这会降低iamge的质量。 这仍然可以,因为您可以使用强制ARGB_8888位图
options.inPrefferedConfig = Bitmap.Config.ARGB_8888 options.inDither = false
真正的问题出现在图像的每个像素都具有255的alpha值(即完全不透明)的时候。 在这种情况下,Bitmap的标志'hasAlpha'被设置为false,即使你的位图有ARGB_8888configuration。 如果你的* .png文件至less有一个真正的透明像素,这个标志将被设置为true,你不必担心任何事情。
所以当你想创build一个缩放的位图使用
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 (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); }
现在这是我的解决scheme。 它不涉及任何复制位图数据:
-
如果当前位图实现具有公共“setHasAplha”方法,则在运行时使用java.lang.Reflect进行检查。 (根据我的testing,从API级别3开始工作完美,而且我还没有testing过更低的版本,因为JNI不会工作)。 如果制造商明确将其私有,保护或删除,则可能会有问题。
-
使用JNI为给定的Bitmap对象调用“setHasAlpha”方法。 这完美的工作,即使是私人的方法或领域。 JNI并没有检查你是否违反访问控制规则。 资料来源: http : //java.sun.com/docs/books/jni/html/pitfalls.html (10.9)这给了我们很大的力量,应该明智地使用它。 我不会尝试修改最后一个字段,即使它能工作(只是举个例子)。 请注意这只是一个解决方法…
这里是我实现所有必要的方法:
JAVA部分:
// 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; }
加载你的lib并声明本地方法:
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 }
而已。 我们完了。 我已经发布了用于复制和粘贴目的的整个代码。 实际的代码不是那么大,但是使所有这些偏执的错误检查使它变得更大。 我希望这可以帮助任何人。
onScreenResults = Bitmap.createScaledBitmap(tempBitmap, scaledOSRW, scaledOSRH, true); <----
设置filter为true为我工作。
因此,不可变的位图上的createScaledBitmap和createBitmap(带有缩放matrix)(如解码时)会忽略原始的Bitmap.Config,如果原始图像没有任何透明度(hasAlpha == false),则使用Bitmap.Config.ARGB_565创build位图。 但是它不会在可变位图上执行。 所以,如果你的解码位图是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();
现在你可以重新调整温度,它应该保留Bitmap.Config.ARGB_8888。
图像缩放也可以通过这种方式完成,绝对没有质量损失!
//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.