Проблемы с качеством при изменении размера изображения во время выполнения

У меня есть файл образа на диске, и я изменяю размер файла и сохраняю его обратно на диск в качестве нового файла изображения. Ради этого вопроса я не привожу их в память, чтобы отображать их на экране, только чтобы изменить их размер и сохранить их. Все это работает отлично. Однако масштабированные изображения имеют на них артефакты, как показано здесь: android: качество изображений, измененных во время выполнения

Они сохраняются с этим искажением, так как я могу вытащить их с диска и посмотреть на них на моем компьютере, и у них все еще есть одна и та же проблема.

Я использую код, подобный этому Странная проблема с памятью при загрузке изображения в объект 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);

Наконец, новое измененное изображение сохраняется на диск с помощью:

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 и, как упоминалось в очень первый связанный пост выше. Это ничего не изменило. Кроме того, в первом посте, которое я связал, Ромен Гай сказал:

Вместо изменения размера во время рисования (что будет очень дорогостоящим), попробуйте изменить размер в растровом изображении вне экрана и убедитесь, что битмап 32 бита (ARGB888).

Однако я не знаю, как убедиться, что битмап остается 32 бита в течение всего процесса.

Я также прочитал пару других статей, таких как http://android.nakatome.net/2010/04/bitmap-basics.html, но все они, казалось, обращались к чертежу и отображению растрового изображения, Я просто хочу изменить его размер и сохранить его обратно на диск без этой проблемы с качеством.

Спасибо большое

Ответ 1

После эксперимента я наконец нашел способ сделать это с результатами хорошего качества. Я напишу это для всех, кто может найти этот ответ полезным в будущем.

Чтобы решить первую проблему, артефакты и странное сглаживание, введенные в изображения, вам необходимо застраховать ваше изображение как 32-битное изображение ARGB_8888. Используя код в моем вопросе, вы можете просто добавить эту строку к параметрам перед вторым декодированием.

options.inPreferredConfig = Bitmap.Config.ARGB_8888;

Добавив, что артефакты исчезли, но ребра по всем изображениям появились через зубчатые, а не четкие. После нескольких экспериментов я обнаружил, что изменение размера растрового изображения с использованием матрицы вместо Bitmap.createScaledBitmap дает гораздо более четкие результаты.

Благодаря этим двум решениям изображения теперь отлично изменяются. Ниже приведен код, который я использую, если он приносит пользу кому-то другому, который сталкивается с этой проблемой.

// Get the source image 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: после постоянной работы над этим я обнаружил, что изображения по-прежнему не на 100% идеальны. Я сделаю обновление, если смогу его улучшить.

Обновление: После того, как вы подтвердили это, я нашел этот вопрос в формате SO и был ответ, в котором упоминалась опция inScaled. Это также помогло качеству, поэтому я добавил обновленный ответ выше, чтобы включить его. Я также теперь нул растровые изображения после того, как они были сделаны.

Кроме того, в качестве побочного примечания, если вы используете эти изображения в WebView, убедитесь, что вы принимаете этот пост во внимание.

Примечание. Вы также должны добавить проверку, чтобы убедиться, что ширина и высота являются действительными числами (не -1). Если они есть, это приведет к тому, что цикл inSampleSize станет бесконечным.

Ответ 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 нетронутой. ПРИМЕЧАНИЕ. Этот код только декодирует растровые изображения и должен быть расширен, поэтому вы можете сохранить битмап.

Я предполагаю, что вы пишете код для версии Android ниже 3.2 (уровень API < 12), поскольку с тех пор поведение методов

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

изменился.

На более старых платформах (уровень API < 12) методы BitmapFactory.decodeFile(..) пытаются вернуть Bitmap с конфигурацией RGB_565 по умолчанию, если они не могут найти альфа, что снижает качество iamge. Это все еще нормально, потому что вы можете использовать растровое изображение ARGB_8888, используя

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

Реальная проблема возникает, когда каждый пиксель вашего изображения имеет альфа-значение 255 (т.е. полностью непрозрачно). В этом случае флаг Bitmap '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);
}

Теперь вот мое решение. Это не связано с копированием растровых данных:

  • Проверено во время выполнения с помощью java.lang.Reflect, если текущий Реализация битового массива имеет общедоступный метод setHasAplha. (По моим тестам он отлично работает с уровня API 3, и я не тестировал более низкие версии, потому что JNI не работал). У вас могут возникнуть проблемы, если производитель явно сделал это конфиденциальным, защищенным или удалил его.

  • Вызвать метод setHasAlpha для данного объекта Bitmap с использованием JNI. Это прекрасно работает даже для частных методов или полей. Официально, что 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
}

Что это. Мы сделали. Я опубликовал весь код для целей копирования и вставки. Фактический код не такой большой, но все эти проверки параноидальных ошибок делают его намного больше. Надеюсь, это может быть полезно для всех.

Ответ 5

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

настройка фильтра на 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.