2012年8月23日木曜日

Android 大きい画像を効果的に読み込む

Loading Large Bitmaps Efficiently
の内容なのですが、補足も入れてメモっておきます。

端的にいうと、

実際に表示するサイズより大きい Bitmap を読みこむのはメモリの無駄
(拡大させるとかなら話は別だけど)

・高解像度のカメラで取られた写真は往々にしてディスプレイのピクセルサイズより大きい
・サムネイルとして使うのに元のサイズのまま読み込むのはばかげてる
・大きいサイズの Bitmap をメモリに展開したら OutOfMemoryException になる


ステップとしては3つ

1. メモリに Bitmap 展開せずに、サイズや MimeType だけを取得する
2. 1. の情報をもとにサブサンプルにサイズを決める
3. 2. で決めたサブサンプルで Bitmap をメモリに読み込む


1. メモリに Bitmap 展開せずに、サイズや MimeType だけを取得する

BitmapFactory のデコードメソッド(decodeFile(), decodeResources(), decodeByteArray() など)は引数として BitmapFactory.Options を取るようになっているものがあります。

この BitmapFactory.Options の inJustDecodeBounds プロパティに true をセットしておくと、Bitmap がメモリに展開されません。ただし、BitmapFactory.Options の outHeight, outWidth, outMimeType プロパティには読み込んだ画像の情報がセットされます。

BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(filePath, options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; String imageType = options.outMimeType;


2. 1. の情報をもとにサブサンプルにサイズを決める

画像のサイズがわかったので、縮小して読み込むのかそのまま読み込むのかを決めます。

・そのまま読み込んだ場合に使われるメモリサイズ
・読み込む画像に割り当てたいメモリサイズ(アプリの他の要因などから決まる)
・読み込んだ画像をセットする ImageView の大きさ
・デバイスの画面サイズとピクセル密度

このあたりの要因から決めます。
よっぽど ImageView が大きくなければ、だいたいは ImageView の大きさで決めます。

デコーダーに画像をサブサンプルさせる(縮小して読み込む、低解像度で読み込む)には、BitmapFactory.Options の inSampleSize パラメータに 1 より大きい整数値を指定します。
(原文ではなぜか true をセットと書いてありますが間違いです)

inSampleSize に 1 より大きい整数値を指定すると、縦横それぞれが約 1 / inSampleSize になって読み込まれます。
例えば

元 : 2048 x 1536
|
inSampleSize = 4

Bitmap : 512 x 384

縦横がそれぞれ 1 / inSampleSize になるので、サイズは 1 / inSampleSize^2 になります。


表示するサイズから inSampleSize を求めるにはこうします。

public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { // 画像の元サイズ final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { if (width > height) { inSampleSize = Math.round((float)height / (float)reqHeight); } else { inSampleSize = Math.round((float)width / (float)reqWidth); } } return inSampleSize; }

Math.round() だと四捨五入なので、切り上げられる場合、表示されるサイズよりも得られる Bitmap のサイズが小さくなります。それが嫌な場合は Math.floor() を使って切り下げるようにします。

inSampleSize には 2 のべき乗を指定したほうが効果的に速くデコードできますが、得られた Bitmap をメモリやディスクにキャッシュするなら、一番近いサイズになるように inSampeSize を決めたほうがいいです。

inSampleSize は 2 のべき乗にして大雑把に縮小し、そのあと Matrix を使って目的のサイズに近い Bitmap を取得する方法もあります。
createBitmap | Android Developers
Matrix | Android Developers



3. 2. で決めたサブサンプルで Bitmap をメモリに読み込む

BitmapFactory.Options の inFustDecodeBounds を false に戻して、2. で決めた値を inSampleSize にセットしてデコードします。

public static Bitmap decodeSampledBitmapFromFile(String filePath, int reqWidth, int reqHeight) { // inJustDecodeBounds=true で画像のサイズをチェック final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(filePath, options); // inSampleSize を計算 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // inSampleSize をセットしてデコード options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(filePath, options); }

例えば、 <resources> <dimen name="image_size">48dip</dimen> </resources> <ImageView android:id="@+id/image" android:layout_width="@dimen/image_size" android:layout_height="@dimen/image_size" />

という ImageView なら、

・View の getWidth(), getHeight() からサイズを取得
・ImageView に対して measure() を呼んで getMeasuredWidth(), getMeasuredHeight() からサイズを取得
・Resources の getDimensionPixelSize() を使って image_size の大きさを取得

という方法があります。
ImageView の大きさが固定値なら、最後の方法がいいと思います。
大きさが可変なら、最初か2番目の方法を使います。

Resources res = getResources(); int size = res.getDimensionPixelSize(R.dimen.image_size); mImageView.setImageBitmap( decodeSampledBitmapFromFile(filePath, size, size));



1 件のコメント:

  1. いつも勉強させていただいてます。

    BitmapFactory.Options.inSampleSizeですが、いつの間にかドキュメントが修正されたようで、
    2013年9月現在「If set to a value > 1…」のように正しい表現になっていまいしたのでご報告です。

    返信削除