Проблемы с качеством при изменении размера изображения во время выполнения - PullRequest
27 голосов
/ 20 ноября 2010

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

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

Я использую код, подобный этому Странно нехватка памяти при загрузке изображения в объект точечного рисунка для декодирования точечного рисунка в память:

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 и просто сохраню sampleledSrcBitmap обратно на диск, проблем не будет, похоже, это произойдет только при изменении размера.

Я пытался, как вы можете видеть в коде, установить для параметра «Разное» значение false, как указано здесь http://groups.google.com/group/android-developers/browse_thread/thread/8b1abdbe881f9f71 и как упомянуто в самом первом связанном посте выше. Это ничего не изменило. Кроме того, в первом посте, на который я ссылался, Ромен Гай сказал:

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

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

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

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

Ответы [ 7 ]

54 голосов
/ 23 ноября 2010

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

Чтобы решить первую проблему, артефакты и странное размывание, появившиеся на изображениях, вам нужно убедиться, что ваше изображение остается как 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: /376948/problema-s-nehvatkoi-pamyati-pri-zagruzke-izobrazheniya-v-rastrovyi-obekt#376958
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 станет бесконечным.

8 голосов
/ 18 марта 2011

В моей ситуации я рисую изображение на экране. Вот что я сделал, чтобы мои изображения выглядели правильно (комбинация ответа 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 голосов
/ 24 ноября 2012

Я создал простую библиотеку, основанную на ответе littleFluffyKitty, которая изменяет размеры и выполняет некоторые другие функции, такие как обрезка и поворот, поэтому, пожалуйста, свободно используйте и улучшайте ее - Android-ImageResizer .

2 голосов
/ 10 августа 2015
onScreenResults = Bitmap.createScaledBitmap(tempBitmap, scaledOSRW, scaledOSRH, true);  <----

установка фильтра на true сработала для меня.

2 голосов
/ 30 августа 2012

"Однако я понятия не имею, как сделать так, чтобы растровое изображение оставалось 32-битным через весь процесс. "

Я хотел опубликовать альтернативное решение, которое позаботится о том, чтобы конфигурация ARGB_8888 оставалась нетронутой. ПРИМЕЧАНИЕ. Этот код декодирует только растровые изображения и должен быть расширен, чтобы вы могли сохранить растровое изображение.

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

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

изменилось.

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

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

Настоящая проблема возникает, когда каждый пиксель вашего изображения имеет альфа-значение 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, если текущий Реализация растрового изображения имеет открытый метод setHasAplha. (Согласно моим тестам, он отлично работает, начиная с уровня API 3, и я не тестировал более низкие версии, потому что JNI не будет работать). У вас могут возникнуть проблемы, если производитель явно сделал его закрытым, защитил или удалил его.

  2. Вызовите метод setHasAlpha для данного объекта Bitmap, используя JNI. Это работает отлично, даже для частных методов или полей. Официально JNI не проверяет, нарушаете ли вы правила контроля доступа или нет. Источник: http://java.sun.com/docs/books/jni/html/pitfalls.html (10,9) Это дает нам большую силу, которую следует использовать с умом. Я бы не стал пытаться изменить конечное поле, даже если бы оно работало (просто для примера). И обратите внимание, это всего лишь обходной путь ...

Вот моя реализация всех необходимых методов:

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

Загрузите вашу библиотеку и объявите собственный метод:

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
}

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

0 голосов
/ 10 января 2013

Масштабирование изображения также может быть достигнуто этим способом без потери качества!

      //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.
0 голосов
/ 25 октября 2012

Итак, 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.

...