运行时调整图像大小时的质量问题

29

我在磁盘上有一张图片文件,我正在将其调整大小并保存为新的图片文件。出于这个问题的考虑,我不会将它们带入内存以便在屏幕上显示,只是为了调整大小和重新保存。这一切都很顺利。然而,缩放后的图像上有像这里显示的伪影: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说:

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

然而,我不知道如何确保整个过程中Bitmap保持32位。

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

非常感谢


你是否正在调整图像大小以解决不同屏幕密度的问题?如果是这样,我猜你会在初始启动时运行此代码,而不是后续启动...或者你只是在本地运行它以获取新的资源(即图像)用于小屏幕?我只是好奇,因为我在小密度屏幕上遇到了错误。 - gary
在我的情况下,这并不是关于屏幕密度的问题。我只是试图将一组给定的图像适配到特定的容器中。 - cottonBallPaws
7个回答

55

经过尝试,我终于找到了一种方法,可以产生高质量的结果。 我将为任何未来可能发现这个答案有用的人撰写本文。

为了解决第一个问题,即图像中引入的伪影和奇怪的抖动,您需要确保您的图像保持为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://dev59.com/puo6XIcBkEYKwwoYTzNK#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%完美的。如果我能改进它,我会更新。

更新:在重新审视此问题后,我在SO上找到了这个问题,其中有一个答案提到了inScaled选项。这也有助于提高质量,因此我更新了上面的答案以包括它。我现在还将位图清空,因为它们使用完毕后不再需要。

另外,顺带一提,如果您在WebView中使用这些图像,请确保考虑此帖子

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


如果您想要ARGB_8888质量,可以检查此答案:http://stackoverflow.com/questions/3440690/rotating-a-bitmap-using-matrix - Rafael Sanches
这对我有用,谢谢!缩略图效果好多了。你可能想要删除 srcHeight 变量,因为它没有被使用。 - Rafael Slobodian
这个工作得很好,但唯一的问题是当要调整大小的图像处于垂直位置时,调整大小后它会变成横向的。有什么想法可以防止这种情况发生吗? - John Ernest Guadalupe

8

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

希望其他人也会发现这个有用。我希望只有两个选项:“是的,让我的调整大小的图像看起来像垃圾”和“不,请不要强制我的用户用勺子挖出他们的眼睛”,而不是各种各样的选项。我知道他们想给我们很多控制权,但也许一些常见设置的辅助方法会很有用。


运行得很好。谢谢 :) 你救了我的一天。 - fahim ayat

3

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


2
“然而,我不知道如何确保Bitmap在整个过程中保持为32位。”
我想提供一种替代方案,可以确保ARGB_8888配置不受影响。注意:此代码仅解码位图,需要扩展以便可以存储位图。
我假设您正在编写适用于Android版本低于3.2(API级别<12)的代码,因为自那时起方法的行为已经改变。
BitmapFactory.decodeFile(pathToImage);
BitmapFactory.decodeFile(pathToImage, opt);
bitmapObject.createScaledBitmap(bitmap, desiredWidth, desiredHeight, false /*filter?*/);

已更改。

在旧平台上(API级别<12),BitmapFactory.decodeFile(..)方法默认尝试返回RGB_565配置的位图,如果找不到任何alpha,则会降低图像质量。这仍然可以接受,因为您可以使用以下方法强制使用ARGB_8888位图:

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

当您的图像每个像素的Alpha值为255(即完全不透明)时,真正的问题就出现了。在这种情况下,位图的标志“hasAlpha”设置为false,即使您的位图具有ARGB_8888配置。如果您的*.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检查当前Bitmap实现是否具有公共的“setHasAlpha”方法。 (根据我的测试,自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
}

就是这样,我们完成了。我已经发布了整个代码,以便复制和粘贴。 实际代码并不是很大,但是进行所有这些偏执错误检查使其变得更大。我希望这对任何人都有所帮助。


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

将过滤器设置为true 对我起了作用。


一样,将标志从 false 改为 true 解决了我的问题。 - Yoann Hercouet

0

使用这种方法还可以实现图像缩放,而且绝对不会损失质量!

      //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类?我不知道有这个名称的类。谢谢 - cottonBallPaws
1
糟糕,我应该说明这是 droidText 或 iText 源/库的一部分。我曾经也将我的图像保存为 pdf。Image 类提供了许多有用的方法。也许由于 droidText 是开源的,你可以仅提取 Image 类以进行图像操作。缩放质量非常好! - mbp

0

因此,在不可变位图上使用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();

现在你可以重新调整temp的大小,它应该保留Bitmap.Config.ARGB_8888。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接