it-swarm.com.de

Qualitätsprobleme bei der Größenänderung eines Bildes zur Laufzeit

Ich habe eine Image-Datei auf der Festplatte und die Größe der Datei wird geändert und als neue Image-Datei auf der Festplatte gespeichert. Aus Gründen dieser Frage bringe ich sie nicht in Erinnerung, um sie auf dem Bildschirm anzuzeigen, nur um sie zu verkleinern und neu zu speichern. Das alles funktioniert gut. Die skalierten Bilder weisen jedoch Artefakte auf, wie hier gezeigt: Android: Qualität der Bilder, die zur Laufzeit geändert werden

Sie werden mit dieser Verzerrung gespeichert, da ich sie von der Festplatte ziehen und auf meinem Computer betrachten kann, und sie haben immer noch das gleiche Problem.

Ich verwende Code, der diesem ähnlich ist Strange out of memory Problem beim Laden eines Bildes in ein Bitmap-Objekt , um die Bitmap in den Speicher zu decodieren:

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

Dann mache ich die eigentliche Skalierung mit:

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

Zuletzt wird das neue Bild mit der folgenden Größe auf der Festplatte gespeichert:

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

Wenn ich dann die Datei von der Diskette ziehe und sie mir anschaue, dann hat sie, wie gesagt, das Qualitätsproblem, das oben verlinkt ist und furchtbar aussieht. Wenn ich die createScaledBitmap überspringe und die SampledSrcBitmap direkt wieder auf der Festplatte speichere, gibt es kein Problem. Es scheint nur zu passieren, wenn sich die Größe ändert.

Wie Sie im Code sehen können, habe ich versucht, inDither auf false zu setzen, wie hier erwähnt http://groups.google.com/group/Android-developers/browse_thread/thread/8b1abdbe881f9f71 und wie in der allererste verknüpfte Post oben. Das hat nichts geändert. In dem ersten Beitrag, den ich verlinkt habe, sagte Romain Guy: 

Anstatt die Größe zum Zeitpunkt des Zeichnens Zu ändern (was sehr kostspielig sein wird), versuchen Sie , Die Größe in einem Offscreen-Bitmap Zu ändern, und stellen Sie sicher, dass die Bitmap 32 Bit Hat. ] (ARGB888).

Ich habe jedoch keine Idee, wie ich sicherstellen kann, dass die Bitmap während des gesamten Prozesses 32 Bit lang bleibt.

Ich habe auch ein paar andere Artikel wie diese http://Android.nakatome.net/2010/04/bitmap-basics.html gelesen, aber alle schienen sich mit dem Zeichnen und Anzeigen der Bitmap zu befassen, ich Ich möchte nur die Größe ändern und sie ohne dieses Qualitätsproblem auf der Festplatte speichern.

Vielen Dank

27
cottonBallPaws

Nach dem Experimentieren habe ich endlich einen Weg gefunden, dies mit guten Ergebnissen zu erreichen. Ich schreibe das für jeden auf, der diese Antwort in der Zukunft als hilfreich erachtet.

Um das erste Problem, die Artefakte und das seltsame Dithering der Bilder zu lösen, müssen Sie sicherstellen, dass Ihr Bild als 32-Bit-Bild mit dem Typ ARGB_8888 erhalten bleibt. Mit dem Code in meiner Frage können Sie diese Zeile vor der zweiten Dekodierung einfach zu den Optionen hinzufügen.

options.inPreferredConfig = Bitmap.Config.ARGB_8888;

Nach dem Hinzufügen der Artefakte waren die Artefakte verschwunden, aber die Ränder in den Bildern wurden durchgezackt anstatt knackig. Nach einigen weiteren Experimenten entdeckte ich, dass die Größenänderung der Bitmap mit einer Matrix anstelle von Bitmap.createScaledBitmap zu viel schärferen Ergebnissen führte.

Mit diesen beiden Lösungen werden die Bilder nun perfekt skaliert. Nachfolgend finden Sie den Code, den ich verwende, falls es jemandem nützt, der auf dieses Problem stößt.

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

BEARBEITEN: Nach kontinuierlicher Arbeit habe ich festgestellt, dass die Bilder immer noch nicht zu 100% perfekt sind. Ich werde ein Update machen, wenn ich es verbessern kann.

Update: Nachdem ich dies revidiert hatte, fand ich diese Frage in SO und es gab eine Antwort auf die inScaled-Option. Dies half auch bei der Qualität, daher fügte ich der obigen Antwort ein Update hinzu. Ich mache jetzt auch die Bitmaps auf Null, nachdem sie fertig sind.

Wenn Sie diese Bilder in einem WebView verwenden, stellen Sie außerdem sicher, dass Sie diesen Beitrag in Betracht ziehen.

Hinweis: Sie sollten auch einen Haken hinzufügen, um sicherzustellen, dass Breite und Höhe gültige Zahlen sind (nicht -1). Wenn dies der Fall ist, wird die inSampleSize-Schleife unendlich.

54
cottonBallPaws

In meiner Situation zeichne ich das Bild auf den Bildschirm. Hier ist das, was ich getan habe, um meine Bilder richtig aussehen zu lassen (eine Kombination aus der Antwort von littleFluffyKitty plus ein paar andere Dinge).

Für meine Optionen, wenn ich das Bild tatsächlich lade (mit decodeResource), stelle ich folgende Werte ein:

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

Wenn ich das Bild tatsächlich zeichne, richte ich mein Paint-Objekt folgendermaßen ein:

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

Hoffentlich findet das auch jemand anderes nützlich. Ich wünschte, es gäbe nur Optionen für "Ja, lassen Sie meine verkleinerten Bilder wie Müll aussehen" und "Nein, bitte zwingen Sie meine Benutzer nicht dazu, ihre Augen mit Löffeln herauszureißen", anstelle der unzähligen verschiedenen Optionen. Ich weiß, dass sie uns viel Kontrolle geben wollen, aber vielleicht helfen einige Hilfsmethoden für die allgemeinen Einstellungen.

8
Jon Turner

Ich habe eine einfache Bibliothek basierend auf littleFluffyKitty answer erstellt, die die Größe ändert und einige andere Dinge wie Zuschneiden und Rotation ausführt, also bitte frei verwenden und verbessern - Android-ImageResizer .

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

den Filter auf true zu setzen, hat für mich funktioniert.

2
loco

"Ich habe jedoch keine Ahnung, wie ich sicherstellen kann, dass die Bitmap während des gesamten Prozesses 32 Bit Bleibt."

Ich wollte eine alternative Lösung posten, die dafür sorgt, dass die ARGB_8888-Konfiguration unangetastet bleibt. HINWEIS: Dieser Code dekodiert nur Bitmaps und muss erweitert werden, damit Sie eine Bitmap speichern können.

Ich gehe davon aus, dass Sie Code für eine Android-Version unter 3.2 (API-Ebene <12) schreiben, da sich seitdem das Verhalten der Methoden ändert

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

hat sich verändert.

Auf älteren Plattformen (API-Ebene <12) versuchen die BitmapFactory.decodeFile (..) -Methoden standardmäßig, eine Bitmap mit der RGB_565-Konfiguration zurückzugeben, wenn sie kein Alpha finden können, was die Qualität eines iamge beeinträchtigt. Dies ist immer noch in Ordnung, da Sie eine ARGB_8888-Bitmap mit erzwingen können

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

Das eigentliche Problem tritt auf, wenn jedes Pixel Ihres Bildes einen Alpha-Wert von 255 hat (d. H. Vollständig undurchsichtig). In diesem Fall wird das Flag 'hasAlpha' der Bitmap auf 'false' gesetzt, auch wenn Ihre Bitmap über eine ARGB_8888-Konfiguration verfügt. Wenn Ihre * .png-Datei mindestens ein echtes transparentes Pixel hätte, wäre dieses Flag auf true gesetzt worden, und Sie müssen sich um nichts kümmern. 

Wenn Sie also eine skalierte Bitmap mit erstellen möchten

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

die Methode prüft, ob das 'hasAlpha'-Flag auf true oder false gesetzt ist, und in Ihrem Fall auf false, um eine skalierte Bitmap zu erhalten, die automatisch in das RGB_565-Format konvertiert wurde.

Daher wird auf API-Ebene> = 12 eine öffentliche Methode aufgerufen

public void setHasAlpha (boolean hasAlpha);

was hätte dieses Problem gelöst. Bisher war dies nur eine Erklärung für das Problem. Ich habe einige Nachforschungen angestellt und festgestellt, dass die setHasAlpha-Methode seit langem existiert und öffentlich ist, aber verborgen wurde (@hide-Anmerkung). So wird es auf Android 2.3 definiert:

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

Hier ist mein Lösungsvorschlag. Es beinhaltet kein Kopieren von Bitmap-Daten:

  1. Wird zur Laufzeit mit Java.lang.Reflect überprüft, ob die aktuelle Bitmap-Implementierung eine öffentliche 'setHasAplha' -Methode hat. (Laut meinen Tests funktioniert es seit API Level 3 einwandfrei und ich habe keine niedrigeren Versionen getestet, da JNI nicht funktionieren würde). Möglicherweise haben Sie Probleme, wenn ein Hersteller ihn explizit als privat festgelegt, geschützt oder gelöscht hat.

  2. Rufen Sie die Methode 'setHasAlpha' für ein bestimmtes Bitmap-Objekt mit JNI auf. Dies funktioniert auch für private Methoden oder Felder einwandfrei. Es ist offiziell, dass JNI nicht überprüft, ob Sie gegen die Zugriffssteuerungsregeln verstoßen oder nicht. Quelle: http://Java.Sun.com/docs/books/jni/html/pitfalls.html (10.9) Dies gibt uns eine große Macht, die vernünftig eingesetzt werden sollte. Ich würde nicht versuchen, ein abschließendes Feld zu ändern, selbst wenn es funktionieren würde (nur um ein Beispiel zu geben). Und bitte beachten Sie, dass dies nur ein Workaround ist ...

Hier ist meine Implementierung aller notwendigen Methoden:

Java-TEIL:

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

Laden Sie Ihre lib und deklarieren Sie die native Methode:

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

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

Native Abschnitt ('Jni'-Ordner)

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
}

Das ist es. Wir sind fertig. Ich habe den gesamten Code zum Kopieren und Einfügen bereitgestellt. Der eigentliche Code ist nicht so groß, aber alle diese paranoiden Fehlerprüfungen machen ihn viel größer. Ich hoffe, das könnte für jeden hilfreich sein.

2
Ivo

Die Bildskalierung kann auf diese Weise auch ohne Qualitätsverlust erfolgen!

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

Daher werden createScaledBitmap und createBitmap (mit einer Matrix, die skaliert) in unveränderlichen Bitmaps (wie beim Dekodieren) ignoriert, das ursprüngliche Bitmap.Config ignorieren und ein Bitmap mit Bitmap.Config.ARGB_565 erstellen, wenn das Original keine Transparenz aufweist (hasAlpha == false). .____.] Aber es wird nicht in einer veränderlichen Bitmap ausgeführt. Wenn Ihre decodierte Bitmap also b ist:

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

Jetzt können Sie die Zeit neu skalieren und Bitmap.Config.ARGB_8888 beibehalten.

0
CloudWalker