2010-11-20 29 views
26

Mam plik obrazu na dysku i zmieniam rozmiar pliku i zapisuję go na dysku jako nowy plik obrazu. W trosce o to pytanie nie wprowadzam ich do pamięci, aby wyświetlić je na ekranie, tylko po to, aby zmienić ich rozmiar i odzyskać. To wszystko działa dobrze. Jednak skalowane obrazy mają na nich artefakty, jak pokazano: android: quality of the images resized in runtimeProblemy z jakością podczas zmiany rozmiaru obrazu w czasie wykonywania

Są one zapisywane z tym zniekształceniem, ponieważ mogę je ściągnąć z dysku i spojrzeć na nie na moim komputerze i nadal mają ten sam problem.

używam kodu podobnego do tego Strange out of memory issue while loading an image to a Bitmap object zdekodować bitmapy w pamięci:

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

Potem robię rzeczywistą skalowanie z:

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

Wreszcie nowy przeskalowany obraz jest zapisywany na dysk z:

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

Następnie, jak już wspomniałem, jeśli pociągnę odkładając plik na dysk i patrząc na niego, ma ten problem z jakością, który jest powiązany powyżej i wygląda okropnie. Jeśli pominiemy createScaledBitmap i po prostu zapiszę sampledSrcBitmap z powrotem na dysk, nie ma problemu, wydaje się, że dzieje się tak tylko przy zmianie wielkości.

Próbowałem, jak widać w kodzie, ustawienie inDither na false, jak wspomniano tutaj http://groups.google.com/group/android-developers/browse_thread/thread/8b1abdbe881f9f71 i jak wspomniano w pierwszym, połączonym wpisie powyżej. To niczego nie zmieniło. Ponadto, w pierwszym poście połączonego Romain Guy powiedział:

zamiast rozmiaru na czas (co będzie bardzo kosztowne), rysunek, próby zmiany rozmiaru w niewidocznej bitmapy i upewnij się, że bitmapy to 32 bity (ARGB888).

Jednak nie mam pojęcia, jak upewnić się, że bitmap pozostanie jako 32 bity przez cały proces.

Przeczytałem również kilka innych artykułów, takich jak ten http://android.nakatome.net/2010/04/bitmap-basics.html, ale wszystkie wydawały się adresować rysunek i wyświetlanie bitmapy, po prostu chcę zmienić rozmiar i zapisać go z powrotem na dysk bez tego problemu z jakością.

Dzięki znacznie

+0

Czy zmieniasz rozmiar obrazów, aby rozwiązać problemy z różnymi gęstościami ekranu? Jeśli tak, domyślam się, że uruchamiasz ten kod przy pierwszym uruchomieniu, a nie po kolejnych uruchomieniach ... Czy po prostu uruchamiasz go lokalnie, aby uzyskać nowe zasoby (czyli obrazy) dla małych ekranów? Jestem ciekawy, ponieważ mam błędy na ekranach o małej gęstości. – gary

+0

W moim przypadku nie chodzi tu o gęstość ekranu. Po prostu próbuję dopasować dany zestaw obrazów do pewnego pojemnika. – cottonBallPaws

Odpowiedz

53

Po eksperymentach I wreszcie znalazł sposób, aby zrobić to z dobrymi wynikami jakości. Napiszę to dla każdego, kto może być przydatny w przyszłości.

Aby rozwiązać pierwszy problem, artefakty i dziwne dithering wprowadzone do obrazów, należy upewnić się, że obraz pozostaje jako 32-bitowy obraz ARGB_8888. Używając kodu z mojego pytania, możesz po prostu dodać tę linię do opcji przed drugim dekodowaniem.

options.inPreferredConfig = Bitmap.Config.ARGB_8888; 

Po dodaniu artefaktów nie było, ale krawędzie na obrazach były poszarpane, a nie ostre. Po dalszych eksperymentach odkryłem, że zmiana rozmiaru mapy bitowej za pomocą macierzy zamiast Bitmap.createScaledBitmap przyniosła znacznie lepsze rezultaty.

Dzięki tym dwóm rozwiązaniom obrazy zmieniają się idealnie.Poniżej znajduje się kod, którego używam na wypadek, gdyby ktoś inny napotkał ten 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; 

EDYCJA: Po ciągłej pracy nad tym stwierdziłem, że obrazy nadal nie są w 100% doskonałe. Zrobię aktualizację, jeśli będę mógł ją poprawić.

Aktualizacja: Po revisting to, znalazłem this question on SO i było odpowiedzią wymieniony opcję inScaled. Pomogło to z jakością, więc dodałem zaktualizowaną odpowiedź powyżej, aby ją uwzględnić. Teraz również zeruję bitmapy po ich użyciu.

Ponadto, jak marginesie, jeśli używasz tych obrazów w WebView, upewnij się wziąć this post into consideration.

Uwaga: należy również dodać upewnij się szerokość i wysokość są ważne numery (nie -1). Jeśli tak, spowoduje to, że pętla inSampleSize stanie się nieskończona.

+0

sprawdź tę odpowiedź, jeśli chcesz jakość ARGB_8888: http://stackoverflow.com/questions/3440690/rotating-a-bitmap-using-matrix –

+0

To zadziałało dla mnie, dzięki! Znacznie lepsze wyniki miniaturek. Możesz usunąć zmienną srcHeight, ponieważ nie jest używana. –

+0

To działało świetnie, ale jedynym problemem jest to, kiedy obraz do zmiany rozmiaru znajduje się w pozycji pionowej, po zmianie rozmiaru staje się zagospodarowany. Jakieś pomysły, jak temu zapobiec? –

8

W mojej sytuacji rysuję obraz na ekranie. Oto, co zrobiłem, aby moje obrazy wyglądały poprawnie (połączenie odpowiedzi LittleFluffyKitty oraz kilku innych rzeczy).

Dla moich opcji, kiedy faktycznie załadować obraz (przy użyciu decodeResource) ustawić następujące wartości:

options.inScaled = false; 
    options.inDither = false; 
    options.inPreferredConfig = Bitmap.Config.ARGB_8888; 

Kiedy faktycznie narysować obraz, skonfigurować moje farby obiekt tak:

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

Mam nadzieję, że ktoś inny uzna to za przydatne. Chciałbym, aby były tylko opcje "Tak, pozwól, aby moje powiększone obrazy wyglądały jak śmieci" i "Nie, proszę, nie zmuszaj moich użytkowników do wyłupywania oczu łyżeczkami" zamiast wszystkich niezliczonych różnych opcji. Wiem, że chcą dać nam dużo kontroli, ale może przydatne będą niektóre metody pomocnicze dla typowych ustawień.

+0

Działa jak urok. Dziękuję :) Uratowałeś mój dzień –

2

"Jednak nie mam pojęcia, jak upewnić się, że bitmap pozostanie jako 32 bity przez cały proces."

Chciałem opublikować alternatywne rozwiązanie, które dba o utrzymanie konfiguracji ARGB_8888 nietkniętej. UWAGA: Ten kod dekoduje bitmapy i wymaga rozszerzenia, więc możesz zapisać bitmapę.

Zakładam, piszesz kod dla wersji Androida niższej niż 3.2 (poziom API < 12), ponieważ od tego czasu zachowanie metod

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

uległ zmianie.

Na starszych platformach (poziom API < 12) metody BitmapFactory.decodeFile (..) próbują domyślnie przywrócić bitmapę z konfiguracją RGB_565, jeśli nie mogą znaleźć żadnej alfa, co obniża jakość elementu iamge. Jest to nadal ok, bo można egzekwowanie bitmapę ARGB_8888 użyciu

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

Prawdziwy problem pojawia się, gdy każdy piksel obrazu ma wartość alfa 255 (czyli całkowicie nieprzezroczysta). W takim przypadku flaga bitmap 'hasAlpha' jest ustawiona na false, nawet jeśli Twoja Bitmapa ma konfigurację ARGB_8888. Jeśli twój plik * .png miał co najmniej jeden prawdziwy przezroczysty piksel, ta flaga byłaby ustawiona na true i nie musiałbyś się o nic martwić.

Więc jeśli chcesz stworzyć skalowany bitmapę korzystając

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

kontrole metod czy flagą „hasAlpha” jest ustawiona na wartość true lub false, aw twoim przypadku jest ona ustawiona na false, co powoduje uzyskanie skalowanej bitmapy, która została automatycznie przekonwertowana do formatu RGB_565.

Dlatego na poziomie API> = 12 istnieje metoda publiczna o nazwie

public void setHasAlpha (boolean hasAlpha); 

które rozwiązały ten problem. Jak dotąd było to tylko wytłumaczenie problemu. Zrobiłem kilka badań i zauważyłem, że metoda setHasAlpha istnieje od dłuższego czasu i jest publiczna, ale została ukryta (@hide adnotacja). Oto, jak to jest zdefiniowane w systemie 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); 
} 

Oto moja propozycja rozwiązania. Nie pociąga za sobą żadnych kopiowaniu danych bitmapy:

  1. sprawdzone w czasie wykonywania przy użyciu java.lang.reflect jeśli obecna implementacja bitmapy ma publiczną metodę „setHasAplha”. (Według moich testów działa idealnie od poziomu API 3 i nie testowałem niższych wersji, ponieważ JNI nie działałoby). Mogą wystąpić problemy, jeśli producent wyraźnie przyznał, że jest prywatny, chroniony lub usunięty.

  2. Wywołanie metody "setHasAlpha" dla danego obiektu Bitmap za pomocą JNI. Działa to doskonale, nawet w przypadku prywatnych metod lub pól. Oficjalne jest, że JNI nie sprawdza, czy naruszasz zasady kontroli dostępu, czy też nie. Źródło: http://java.sun.com/docs/books/jni/html/pitfalls.html (10.9) Daje nam to ogromną moc, która powinna być używana mądrze. Nie próbowałbym modyfikować pola końcowego, nawet gdyby działało (wystarczy podać przykład). I proszę pamiętać, jest to tylko obejście ...

Oto moja wdrożenie wszystkich niezbędnych metod:

JAVA CZĘŚĆ:

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

Załaduj lib i zadeklarować sposób natywny:

static { 
    System.loadLibrary("bitmaputils"); 
} 

private static native int setHasAlphaNative(Bitmap bitmap, boolean value); 

Język odcinek ('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 
} 

To wszystko. Skończyliśmy. Wysłałem cały kod do kopiowania i wklejania. Rzeczywisty kod nie jest aż tak duży, ale sprawdzenie wszystkich paranoicznych błędów sprawia, że ​​jest on o wiele większy. Mam nadzieję, że może to być przydatne dla każdego.

0

Tak więc createScaledBitmap i createBitmap (z macierzą, która skaluje) na niezmiennej mapie bitowej (np. Po dekodowaniu) zignorują oryginalny Bitmap.Config i utworzą bitmapę za pomocą Bitmap.Config.ARGB_565, jeśli oryginał nie ma żadnej przezroczystości (hasAlpha == false). Ale nie zrobi tego na zmienionej bitmapie. Dlatego, jeśli jest dekodowany bitmapy 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(); 

Teraz można przeskalować temp i powinien zachować Bitmap.Config.ARGB_8888.

3

Stworzyłem prostą bibliotekę opartą na littleFluffyKitty odpowiedzi, która zmienia rozmiar i robi kilka innych rzeczy, takich jak przycinanie i obracanie, więc proszę, aby go użyć i poprawić - Android-ImageResizer.

0

Skalowanie obrazu można również osiągnąć dzięki temu bez utraty jakości!

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

Czym jest klasa Obraz? Nie jestem świadomy klasy o tej nazwie. Dzięki – cottonBallPaws

+1

Oop's - moje złe, powinienem był wskazać, że jest częścią droidText lub iText source/library. Zapisuję również moje obrazy w formacie pdf. Klasa Image dostarcza wiele użytecznych metod. Być może to, że droidText to openSource, możesz wyodrębnić wyłącznie klasę Image do manipulacji Image. Jakość skalowania jest natychmiastowa! – mbp

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

ustawienie filtru true pracował dla mnie.

+0

To samo tutaj, zmiana flagi z false na true naprawił mój problem. –

Powiązane problemy