调整/缩放位图后出现的图像质量差

79

我正在编写一款纸牌游戏,并需要根据不同情况使我的牌具有不同的尺寸。为了能够快速地绘制和重绘(用于动画),我将图像存储为位图。

然而,无论我如何尝试缩放我的图像(无论是通过matrix.postScale、matrix.preScale还是createScaledBitmap函数),它们总是变得像素化和模糊。我知道问题出在缩放上,因为没有缩放时绘制的图像看起来很完美。

我已经尝试了这两个线程中描述的每一个解决方案:
android quality of the images resized in runtime
quality problems when resizing an image at runtime

但是仍然没有任何进展。

我使用以下代码将我的位图(存储在哈希表中)进行存储:

cardImages = new HashMap<Byte, Bitmap>();
cardImages.put(GameUtil.hearts_ace, BitmapFactory.decodeResource(r, R.drawable.hearts_ace));

使用这种方法(在Card类中)绘制它们:

public void drawCard(Canvas c)
{
    //retrieve the cards image (if it doesn't already have one)
    if (image == null)
        image = Bitmap.createScaledBitmap(GameUtil.cardImages.get(ID), 
            (int)(GameUtil.standardCardSize.X*scale), (int)(GameUtil.standardCardSize.Y*scale), false);

        //this code (non-scaled) looks perfect
        //image = GameUtil.cardImages.get(ID);

    matrix.reset();
    matrix.setTranslate(position.X, position.Y);

    //These methods make it look worse
    //matrix.preScale(1.3f, 1.3f);
    //matrix.postScale(1.3f, 1.3f);

    //This code makes absolutely no difference
    Paint drawPaint = new Paint();
    drawPaint.setAntiAlias(false);
    drawPaint.setFilterBitmap(false);
    drawPaint.setDither(true);

    c.drawBitmap(image, matrix, drawPaint);
}

任何见解都将不胜感激。谢谢。

当你说“这个代码完全没有任何影响”时,我想你是将 setAntiAlias 方法的参数设置为 true (但它仍然没有任何影响)? - fredley
没错,我已经尝试过对所有这些方法使用true/false,但它们都没有任何区别。 - Cbas
这个问题已经在Meta SE上提到了。 - Linny
11个回答

91

使用createScaledBitmap会使你的图片看起来很糟糕。 我曾经遇到过这个问题并解决了它。 以下代码可以修复这个问题:

public Bitmap BITMAP_RESIZER(Bitmap bitmap,int newWidth,int newHeight) {    
    Bitmap scaledBitmap = Bitmap.createBitmap(newWidth, newHeight, Config.ARGB_8888);

    float ratioX = newWidth / (float) bitmap.getWidth();
    float ratioY = newHeight / (float) bitmap.getHeight();
    float middleX = newWidth / 2.0f;
    float middleY = newHeight / 2.0f;

    Matrix scaleMatrix = new Matrix();
    scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);

    Canvas canvas = new Canvas(scaledBitmap);
    canvas.setMatrix(scaleMatrix);
    canvas.drawBitmap(bitmap, middleX - bitmap.getWidth() / 2, middleY - bitmap.getHeight() / 2, new Paint(Paint.FILTER_BITMAP_FLAG));

    return scaledBitmap;

    }

6
这个解决方案非常完美(对我来说是唯一有效的),但是你可以摆脱 middleXmiddleY:只需将它们设为0,并声明canvas.drawBitmap(bitmap, 0, 0, new Paint(Paint.FILTER_BITMAP_FLAG)); - aberaud
4
以前我用的绘图工具与Paint.FILTER_BITMAP_FLAGS不同。改用Paint.FILTER_BITMAP_FLAG后,我的绘图效果有了极大的提升!谢谢! - Dror
结果有很大的改善,但有时当我在相机拍摄图像后立即尝试使用此方法时,该方法无法正常工作。 - AnkitRox
8
抱歉,但是这个解决方案在我的情况下不起作用。 - sumeet
我尝试了这个解决方案,质量很好,但只返回了位图的四分之一部分。 - Ali Akram
仍然得到模糊的图像,质量仍然很差。 - Mujahid Khan

48

在低屏幕分辨率下,我的图像模糊,直到我禁用了从资源加载位图时的缩放:

Options options = new BitmapFactory.Options();
    options.inScaled = false;
    Bitmap source = BitmapFactory.decodeResource(a.getResources(), path, options);

那个运行得很完美!图像非常清晰,没有模糊。谢谢... 我发现我还需要使用WarrenFaith的方法:滤波图像可以去除锯齿状,将inScaled选项设置为false可以消除模糊。感谢大家的帮助! - Cbas
4
不错的东西。只是需要说明的是,a.getResources() 可以缩短为 getResources(),而 path 是位图的 R.drawable.id。 - Shadoath
太好了,谢谢你的建议。我注释掉了这一行 o2.inSampleSize=scale; 并根据你的建议添加了这一行 ( o2.inScaled = false; )。但是我本来就将比例值设置为1,那么问题出在哪里呢?能否请您详细解释一下? - Sagar Devanga

36

createScaledBitmap有一个标记,可以设置是否应该对缩放后的图像进行滤波处理。该标记可以改善位图的质量...


2
哇,这大大改善了它!谢谢,我简直不敢相信其他帖子上没有人提到这个... 它消除了所有锐利的边缘,但图像仍然非常模糊。有什么办法可以消除模糊? - Cbas

12

用作

mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); 

Paint.FILTER_BITMAP_FLAG 对我起到了作用。


1
不知道为什么这个被踩了,但这对我来说是解决方案。 - Stephane Mathis
2
谢谢,这拯救了我的一天! 我将其用作canvas.drawBitmap方法的参数。 - Couitchy

8

我猜你正在编写适用于 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(即完全不透明)。在这种情况下,即使您的 Bitmap 具有 ARGB_8888 配置,Bitmap 的标志“ 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方法解决。到目前为止,这只是问题的解释。我做了一些研究并注意到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调用给定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;
    }

加载您的库并声明本机方法:

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
}

就是这样,我们完成了。我已经发布了整个代码供复制粘贴使用。

实际的代码并不是那么大,但是进行所有这些谨慎的错误检查会使它变得更大。希望这对任何人都有所帮助。


请问您能否在GitHub上的一个示例Android项目中添加上述代码?您的方法很有趣。 - S.M.Mousavi
抱歉,但我不再具备本地开发的设置。 - Ivo

8
好的缩放算法(不使用最近邻算法,因此不会添加像素化)仅由2个步骤组成(加上计算输入/输出图像裁剪的确切矩形):
1. 使用尽可能接近所需分辨率但不少于它的BitmapFactory.Options::inSampleSize-> BitmapFactory.decodeResource()进行缩小 2. 通过稍微缩小来实现精确分辨率,使用Canvas::drawBitmap() 这里有详细说明,索尼移动如何解决这个任务:https://web.archive.org/web/20171011183652/http://developer.sonymobile.com/2011/06/27/how-to-scale-images-for-your-android-application/ 这里是索尼移动比例工具的源代码: https://web.archive.org/web/20170105181810/http://developer.sonymobile.com:80/downloads/code-example-module/image-scaling-code-example-for-android/

4

如果你将位图放大,永远不会得到完美的结果。

你应该从你需要的最高分辨率开始,并进行缩小。

当对位图进行放大时,缩放无法猜测每个现有点之间的缺失点是什么,因此它要么复制邻居(=> 锐利),要么计算邻居之间的平均值(=> 模糊)。


这完全有道理,虽然使用上述方法,我已经能够获得非常清晰的图像 - 如此清晰,以至于我看不出真实图像和放大后的图像之间的任何区别。我确定它不是完美的,但它绝对看起来像是(我正在使用最新的Android型号进行测试)。不过你说得有道理。如果我想将这个游戏发布到平板电脑上,我一定要考虑这个问题。 - Cbas

4

我在模糊处理时使用了标志filter = true

bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);

1
如果您想获得高质量的结果,那么请使用[RapidDecoder][1]库。使用方法如下:
import rapid.decoder.BitmapDecoder;
...
Bitmap bitmap = BitmapDecoder.from(getResources(), R.drawable.image)
                             .scale(width, height)
                             .useBuiltInDecoder(true)
                             .decode();

不要忘记使用内置解码器,如果您想缩小50%以下并获得高质量的结果。我在API 8上进行了测试。

1
在将Android目标框架从Android 8.1更新到Android 9时,我遇到了这个问题,并且在我的ImageEntryRenderer上表现出来。希望这可以帮助到您。
    public Bitmap ProcessScaleBitMap(Bitmap bitmap, int newWidth, int newHeight)
    {
        newWidth = newWidth * 2;
        newHeight = newHeight * 2;

        Bitmap scaledBitmap = CreateBitmap(newWidth, newHeight, Config.Argb8888);

        float scaleDensity = ((float)Resources.DisplayMetrics.DensityDpi / 160);
        float scaleX = newWidth / (bitmap.Width * scaleDensity);
        float scaleY = newHeight / (bitmap.Height * scaleDensity);

        Matrix scaleMatrix = new Matrix();
        scaleMatrix.SetScale(scaleX, scaleY);

        Canvas canvas = new Canvas(scaledBitmap);
        canvas.Matrix = scaleMatrix;
        canvas.DrawBitmap(bitmap, 0, 0, new Paint(PaintFlags.FilterBitmap));

        return scaledBitmap;
    }

注意:我正在使用Xamarin 3.4.0.10框架进行开发。

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