web-dev-qa-db-ja.com

実行時に画像のサイズを変更するときの品質の問題

ディスクにイメージファイルがあり、ファイルのサイズを変更して、新しいイメージファイルとしてディスクに保存しています。この質問のために、私はそれらを画面に表示するためにメモリに持ってくるのではなく、サイズを変更して再保存するだけです。これはすべてうまくいきます。ただし、スケーリングされた画像には、次に示すようなアーティファクトが含まれています。 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をスキップして、sampledSrcBitmapをディスクに保存し直しても問題がない場合、サイズが変更された場合にのみ発生するようです。

コードでわかるように、ここで述べたようにinDitherをfalseに設定してみました http://groups.google.com/group/Android-developers/browse_thread/thread/8b1abdbe881f9f71 と述べたように上記の最初のリンクされた投稿で。それは何も変わりませんでした。また、私がリンクした最初の投稿で、Romain Guyは次のように述べています。

描画時にサイズを変更するのではなく(非常にコストがかかります)、オフスクリーンビットマップでサイズ変更を試み、ビットマップが32ビット(ARGB888)であることを確認してください。

ただし、プロセス全体でビットマップが32ビットのままであることを確認する方法はわかりません。

私はまた、このような他のいくつかの記事を読んだ http://Android.nakatome.net/2010/04/bitmap-basics.html が、それらはすべて、ビットマップの描画と表示に対処しているようだった。サイズを変更してディスクに保存したいのですが、この品質の問題はありません。

どうもありがとう

28
cottonBallPaws

実験した後、私はようやくこれを高品質の結果で行う方法を見つけました。この回答が将来役立つと思う人のために、これを書いていきます。

最初の問題、画像に導入されたアーティファクトと奇妙なディザリングを解決するには、画像が32ビットARGB_8888画像のままであることを確認する必要があります。私の質問のコードを使用すると、2番目のデコードの前に、この行をオプションに追加するだけで済みます。

options.inPreferredConfig = Bitmap.Config.ARGB_8888;

それを追加した後、アーティファクトはなくなりましたが、画像全体のエッジは鮮明ではなくギザギザになります。さらに実験を重ねた結果、Bitmap.createScaledBitmapの代わりにMatrixを使用してビットマップのサイズを変更すると、より鮮明な結果が得られることがわかりました。

これら2つのソリューションにより、画像のサイズが完全に変更されました。以下は、この問題に遭遇した他の誰かに利益をもたらす場合に使用するコードです。

// 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オプションについての回答がありました。これは品質にも役立ちましたので、上記の回答を更新してそれを含めるように追加しました。また、ビットマップが使用された後、ビットマップをnullにします。

また、付記として、これらの画像をWebViewで使用している場合は、必ず この投稿を考慮に入れてください。

注:幅と高さが有効な数値(-1ではない)であることを確認するチェックも追加する必要があります。そうである場合、inSampleSizeループが無限になります。

55
cottonBallPaws

私の状況では、画面に画像を描画しています。これが私の画像を正しく見せるために私がしたことです(littleFluffyKittyの答えといくつかの他のものの組み合わせ)。

(decodeResourceを使用して)画像を実際に読み込むときのオプションとして、次の値を設定します。

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

実際に画像を描画するときは、次のようにPaintオブジェクトを設定します。

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

うまくいけば、他の誰かもそれが役立つと思います。無数のさまざまなオプションの代わりに、「はい、サイズを変更した画像をゴミのように見せます」と「いいえ、ユーザーにスプーンで目を食らわせないでください」というオプションがあったらいいのにと思います。私は彼らが私たちに多くの制御を与えたいと思っていますが、一般的な設定のためのいくつかのヘルパーメソッドが役に立つかもしれません。

8
Jon Turner

私はlittleFluffyKitty回答に基づいてシンプルなライブラリを作成しました。これはサイズ変更を行い、クロップや回転などの他のことを行います。自由に使用して改善してください Android-ImageResizer

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

フィルターをtrueに設定するとうまくいきました。

2
loco

「しかし、私は、プロセス全体を通してビットマップが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(..)メソッドはデフォルトでRGB_565構成のビットマップを返そうとします。これにより、画像の品質が低下します。次を使用してARGB_8888ビットマップを強制できるため、これはまだ問題ありません。

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

実際の問題は、画像の各ピクセルのアルファ値が255(つまり、完全に不透明)である場合に発生します。その場合、ビットマップにARGB_8888構成がある場合でも、ビットマップのフラグ 'hasAlpha'はfalseに設定されます。 * .pngファイルに少なくとも1つの実際の透明ピクセルがある場合、このフラグは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. 現在のビットマップ実装にパブリックの「setHasAplha」メソッドがあるかどうか、Java.lang.Reflectを使用して実行時にチェックされます。 (私のテストによると、APIレベル3以降は完全に機能し、JNIが機能しないため、それより低いバージョンはテストしていません)。メーカーが明示的に非公開にしたり、保護したり、削除したりすると、問題が発生する可能性があります。

  2. JNIを使​​用して、指定されたビットマップオブジェクトの「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;
    }

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
}

それでおしまい。終わりました。コピーアンドペーストの目的でコード全体を投稿しました。実際のコードはそれほど大きくありませんが、これらすべての偏執的なエラーチェックを行うと、コードが非常に大きくなります。これが誰にとっても役立つことを願っています。

2
Ivo

この方法で画像のスケーリングを実行しても、品質はまったく失われません。

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

そのため、不変のビットマップ(デコード時など)でcreateScaledBitmapおよびcreateBitmap(スケーリングする行列を使用)は、元のBitmap.Configを無視し、元のものが透明度を持たない場合(hasAlpha == false)に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();

これでtempを再スケーリングでき、Bitmap.Config.ARGB_8888を保持するはずです。

0
CloudWalker