在运行时调整图像大小时出现质量问题

发布于 2024-10-03 10:56:06 字数 2172 浏览 3 评论 0原文

我的磁盘上有一个图像文件,我正在调整该文件的大小并将其作为新的图像文件保存回磁盘。为了这个问题,我不会将它们放入内存以便在屏幕上显示它们,只是调整它们的大小并重新保存它们。这一切都很好。但是,缩放后的图像上有伪像,如下所示: android:quality在运行时调整大小的图像

它们在保存时会出现这种失真,因为我可以将它们从磁盘上拉出来并在我的计算机上查看它们,但它们仍然存在相同的问题。

我正在使用类似于此的代码 加载图像时出现奇怪的内存不足问题到位图对象以将位图解码到内存中:

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 以及上面第一个链接帖子中提到的。那并没有改变任何事情。另外,在我链接的第一篇文章中,罗曼·盖伊说:

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

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

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

非常感谢

I have a image file on the disk and I am resizing the file and saving it back to disk as a new image file. For the sake of this question, I am not bringing them into memory in order to display them on the screen, only to resize them and resave them. This all works just fine. However, the scaled images have artifacts on them like shown here: android: quality of the images resized in runtime

They are saved with this distortion, as I can pull them off the disk and look at them on my computer and they still have the same issue.

I am using code similar to this Strange out of memory issue while loading an image to a Bitmap object to decode the bitmap into 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 am doing the actual scaling with:

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

Lastly, the new resized image is saved to disk with:

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

Then, as I mentioned, if I pull that file off the disk and look at it, it has that quality issue linked above and looks terrible. If I skip the createScaledBitmap and just save the sampledSrcBitmap right back to disk there is no problem, it seems to only happen if the size changes.

I have tried, as you can see in the code, setting inDither to false as mentioned here http://groups.google.com/group/android-developers/browse_thread/thread/8b1abdbe881f9f71 and as mentioned in the very first linked post above. That didn't change anything. Also, in the first post I linked, Romain Guy said:

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

However, I have no idea how to make sure the Bitmap stays as 32 bits through the whole process.

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

Thanks much

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

并安 2024-10-10 10:56:06

经过实验,我终于找到了一种方法来做到这一点,并取得了良好的质量结果。我会为将来可能会发现这个答案有帮助的人写下这篇文章。

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

更新:重新查看后,我发现这个问题SO 并且有一个答案提到了 inScaled 选项。这也有助于提高质量,因此我添加更新了上面的答案以包含它。我现在还在使用完位图后将其清空。

另外,作为旁注,如果您在 WebView 中使用这些图像,请确保采用 考虑这篇文章。

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

After experimenting I have finally found a way to do this with good quality results. I'll write this up for anyone that might find this answer helpful in the future.

To solve the first problem, the artifacts and weird dithering introduced into the images, you need to insure your image stays as a 32-bit ARGB_8888 image. Using the code in my question, you can simply add this line to the options before the second decode.

options.inPreferredConfig = Bitmap.Config.ARGB_8888;

After adding that, the artifacts were gone but edges throughout the images came through jagged instead of crisp. After some more experimentation I discovered that resizing the bitmap using a Matrix instead of Bitmap.createScaledBitmap produced much crisper results.

With those two solutions, the images are now resizing perfectly. Below is the code I am using in case it benefits someone else coming across this problem.

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

EDIT: After continual work on this I have found that the images still aren't 100% perfect. I'll make an update if I can improve it.

Update: After revisting this, I found this question on SO and there was an answer that mentioned the inScaled option. This helped with the quality as well so I added updated the answer above to include it. I also now null the bitmaps after they are done being used.

Also, as a side note, if you are using these images in a WebView, make sure you take this post into consideration.

Note: you should also add a check to make sure the width and height are valid numbers (not -1). If they are, it will cause the inSampleSize loop to become infinite.

撑一把青伞 2024-10-10 10:56:06

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

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

In my situation I am drawing the image to the screen. Here's what I did to get my images to look correct (a combination of littleFluffyKitty's answer, plus a few other things).

For my options 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 the image, I set up my paint object like this:

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

Hopefully someone else finds that useful too. I wish there were just options for "Yes, let my resized images look like garbage" and "No, please don't force my users to gouge their eyes out with spoons" instead of all the myriad of different options. I know they want to give us lots of control, but maybe some helper methods for common settings could be useful.

梦一生花开无言 2024-10-10 10:56:06

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

I created simple library based on littleFluffyKitty answer which does resize and does some other things like crop and rotation so please free to use it and improve it - Android-ImageResizer.

还如梦归 2024-10-10 10:56:06

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

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

假设您正在编写代码对于低于 3.2 的 Android 版本(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(即完全不透明),在这种情况下,即使您的位图具有 ARGB_8888 配置,该位图的标志“hasAlpha”也会设置为 false。如果您的 *.png 文件至少有一个真正的透明像素,则该标志将具有 请检查“hasAlpha”标志是否设置为

因此,当您想使用该方法创建缩放位图时,

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

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 调用给定 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:bitmapUtils.c

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)

#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
}

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

"However, I have no idea how to make sure the Bitmap stays as 32 bits
through the whole process."

I wanted to post an alternative solution, which takes care of keeping the ARGB_8888 config untouched. NOTE: This code only decodes bitmaps and needs to be extended, so you could store a Bitmap.

I assume you are writing code for a version of Android lower than 3.2 (API level < 12), because 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 a Bitmap with RGB_565 config by default, if they can't find any alpha, which lowers the quality of an iamge. This is still ok, because you can enforce an ARGB_8888 bitmap using

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

The real problem comes when each pixel of your image has an alpha value of 255 (i.e. completely opaque). In that case the Bitmap's flag 'hasAlpha' is set to false, even though your Bitmap has ARGB_8888 config. If your *.png-file had at least one real transparent pixel, this flag would have been set to true and you wouldn't have to worry about anything.

So when you want to create a scaled Bitmap using

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

the method checks whether the 'hasAlpha' flag is set to true or false, and in your case it is set to false, which results in obtaining a scaled Bitmap, which was automatically converted to the RGB_565 format.

Therefore on API level >= 12 there is a public method called

public void setHasAlpha (boolean hasAlpha);

which would have solved this issue. So far this was just an explanation of the problem.
I did some research and noticed that the setHasAlpha method has existed for a long time and it's public, but has been 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 (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);
}

Now here is my solution proposal. It does not involve any copying of bitmap data:

  1. Checked at runtime using java.lang.Reflect if the current
    Bitmap implementation has a public 'setHasAplha' method.
    (According to my tests it works perfectly since API level 3, and i haven't tested lower versions, because JNI wouldn't work). You may have problems if a manufacturer has explicitly made it private, protected or deleted it.

  2. Call the 'setHasAlpha' method for a given Bitmap object using JNI.
    This works perfectly, even for private methods or fields. It is official that JNI does not check whether you are violating the access control rules or not.
    Source: http://java.sun.com/docs/books/jni/html/pitfalls.html (10.9)
    This gives us great power, which should be used wisely. I wouldn't try modifying a final field, even if it would work (just to give an example). And please note this is just a workaround...

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

Load your lib and declare the native 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
}

That's it. We are done. I've posted the whole code for copy-and-paste purposes.
The actual code isn't that big, but making all these paranoid error checks makes it a lot bigger. I hope this could be helpful to anyone.

迷途知返 2024-10-10 10:56:06
onScreenResults = Bitmap.createScaledBitmap(tempBitmap, scaledOSRW, scaledOSRH, true);  <----

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

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

setting the filter to true worked for me.

菊凝晚露 2024-10-10 10:56:06

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

So, createScaledBitmap and createBitmap (with matrix that scales) on immutable bitmap (like when decoded) will ignore original Bitmap.Config and create bitmap with Bitmap.Config.ARGB_565 if original doesn't have any transparency (hasAlpha == false).
But it won't do it on mutable 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 rescale temp and it should retain Bitmap.Config.ARGB_8888.

や莫失莫忘 2024-10-10 10:56:06

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

      //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 scaling can also be accomplished by this means with absolutely no quality loss!

      //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.
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文