【问题标题】:Quality problems when resizing an image at runtime在运行时调整图像大小时的质量问题
【发布时间】:2011-05-13 00:31:15
【问题描述】:

我在磁盘上有一个图像文件,我正在调整文件大小并将其作为新图像文件保存回磁盘。为了这个问题,我不是为了在屏幕上显示它们而将它们带入内存,只是为了调整它们的大小并重新保存它们。这一切都很好。但是,缩放后的图像上有伪影,如下所示:android: quality of the images resized in runtime

它们被这种失真保存了,因为我可以将它们从磁盘上拉下来并在我的计算机上查看它们,它们仍然存在同样的问题。

我正在使用类似于Strange out of memory issue while loading an image to a Bitmap object 的代码将位图解码到内存中:

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

最后,新调整大小的图像通过以下方式保存到磁盘:

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

然后,正如我所提到的,如果我将该文件从磁盘中取出并查看它,它就会出现上面链接的质量问题并且看起来很糟糕。如果我跳过 createScaledBitmap 而只是将 sampledSrcBitmap 保存回磁盘就没有问题,它似乎只有在大小发生变化时才会发生。

正如您在代码中看到的那样,我已经尝试将 inDither 设置为 false,如此处 http://groups.google.com/group/android-developers/browse_thread/thread/8b1abdbe881f9f71 所述以及上面第一个链接帖子中所述。那并没有改变什么。此外,在我链接的第一篇文章中,Romain Guy 说:

而不是在绘图时调整大小 (这将非常昂贵), 尝试在屏幕外位图中调整大小 并确保位图是 32 位 (ARGB888)。

但是,我不知道如何确保位图在整个过程中保持为 32 位。

我还阅读了其他几篇文章,例如 http://android.nakatome.net/2010/04/bitmap-basics.html,但它们似乎都在解决绘制和显示位图的问题,我只想调整它的大小并将其保存回磁盘,而不会出现这个质量问题。

非常感谢

【问题讨论】:

  • 您是否正在调整图像大小以解决各种屏幕密度的问题?如果是这样,我想您会在初始启动时运行此代码,而不是随后的启动......或者您只是在本地运行它以获得小屏幕的新资产(即图像)?我只是好奇,因为我在小密度屏幕上遇到了错误。
  • 就我而言,这与屏幕密度无关。我只是想将一组给定的图像放入某个容器中。

标签: android image bitmap resize


【解决方案1】:

经过试验,我终于找到了一种方法,可以得到高质量的结果。我会为将来可能会发现此答案有帮助的任何人写这篇文章。

要解决第一个问题,即图像中引入的伪影和奇怪的抖动,您需要确保图像保持为 32 位 ARGB_8888 图像。使用我的问题中的代码,您可以简单地将这一行添加到第二次解码之前的选项中。

options.inPreferredConfig = Bitmap.Config.ARGB_8888;

添加后,伪影消失了,但整个图像的边缘呈锯齿状,而不是清晰。经过更多实验后,我发现使用 Matrix 而不是 Bitmap.createScaledBitmap 调整位图大小会产生更清晰的结果。

通过这两种解决方案,图像现在可以完美地调整大小。以下是我正在使用的代码,以防其他人遇到此问题。

// 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% 完美的。如果我可以改进它,我会进行更新。

更新: 修改后,我找到了this question on SO 并且有一个答案提到了 inScaled 选项。这也有助于提高质量,因此我添加了更新的答案以包含它。我现在也将位图在使用完毕后清空。

另外,附带说明一下,如果您在 WebView 中使用这些图像,请确保您使用 this post into consideration.

注意:您还应该添加检查以确保宽度和高度是有效数字(不是 -1)。如果是,则会导致 inSampleSize 循环变为无限。

【讨论】:

  • 如果您想要 ARGB_8888 质量,请查看此答案:stackoverflow.com/questions/3440690/…
  • 这对我有用,谢谢!更好的缩略图结果。您可能想要删除 srcHeight 变量,因为它没有被使用。
  • 这很好用,但唯一的问题是当要调整大小的图像处于垂直位置时,调整大小后它会变成横向。有什么想法可以防止这种情况发生吗?
【解决方案2】:

在我的情况下,我将图像绘制到屏幕上。这是我为使图像看起来正确所做的工作(结合了 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);

希望其他人也觉得这很有用。我希望只有“是的,让我调整大小的图像看起来像垃圾”和“不,请不要强迫我的用户用勺子挖出他们的眼睛”的选项,而不是所有无数不同的选项。我知道他们想给我们很多控制权,但也许一些常用设置的辅助方法可能有用。

【讨论】:

  • 工作就像一个魅力。谢谢你:) 你拯救了我的一天
【解决方案3】:

我基于littleFluffyKitty 的答案创建了一个简单的库,它会调整大小并执行一些其他操作,例如裁剪和旋转,所以请免费使用并改进它 - Android-ImageResizer

【讨论】:

    【解决方案4】:

    “但是,我不知道如何确保位图保持为 32 位 贯穿整个过程。”

    我想发布一个替代解决方案,它负责保持 ARGB_8888 配置不变。注意:此代码仅解码位图,需要扩展,因此您可以存储位图。

    我假设您正在为低于 3.2(API 级别

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

    变了。

    在旧平台(API 级别 强制执行 ARGB_8888 位图

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

    真正的问题出现在图像的每个像素的 alpha 值为 255(即完全不透明)时。在这种情况下,即使您的位图具有 ARGB_8888 配置,位图的标志“hasAlpha”也会设置为 false。如果您的 *.png 文件至少有一个真正的透明像素,则该标志将设置为 true,您不必担心任何事情。

    所以当你想创建一个缩放位图时使用

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

    现在这是我的解决方案建议。它不涉及任何位图数据的复制:

    1. 在运行时使用 java.lang.Reflect 进行检查,如果当前 位图实现有一个公共的“setHasAplha”方法。 (根据我的测试,它从 API 级别 3 开始就可以完美运行,而且我还没有测试过较低版本,因为 JNI 不起作用)。如果制造商明确将其设为私有、保护或删除,您可能会遇到问题。

    2. 使用 JNI 为给定的位图对象调用“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;
        }
    

    加载你的库并声明本地方法:

    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
    }
    

    就是这样。我们完了。我已经发布了整个代码用于复制和粘贴目的。 实际的代码并没有那么大,但是进行所有这些偏执的错误检查会使它变得更大。我希望这对任何人都有帮助。

    【讨论】:

      【解决方案5】:
      onScreenResults = Bitmap.createScaledBitmap(tempBitmap, scaledOSRW, scaledOSRH, true);  <----
      

      将过滤器设置为 true 对我有用。

      【讨论】:

      • 这里也一样,将标志从 false 更改为 true 解决了我的问题。
      【解决方案6】:

      因此,不可变位图(如解码时)上的 createScaledBitmap 和 createBitmap(带有可缩放的矩阵)将忽略原始 Bitmap.Config 并使用 Bitmap.Config.ARGB_565 创建位图,如果原始位图没有任何透明度(hasAlpha == false )。 但它不会在可变位图上执行此操作。 所以,如果你解码的位图是 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。

      【讨论】:

        【解决方案7】:

        图像缩放也可以通过这种方式完成,绝对没有质量损失!

              //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.
        

        【讨论】:

        • 什么是 Image 类?我不知道那个名字的类。谢谢
        • 糟糕——我的错,我应该指出它是 droidText 或 iText 源/库的一部分。我过去也将我的图像保存为 pdf。 Image 类提供了许多有用的方法。也许由于 droidText 是开源的,您可以仅提取 Image 类进行图像操作。缩放质量非常准确!
        猜你喜欢
        • 2011-01-03
        • 1970-01-01
        • 1970-01-01
        • 2010-10-22
        • 2021-11-06
        • 2017-06-25
        • 1970-01-01
        • 2012-12-05
        • 2017-11-27
        相关资源
        最近更新 更多