在安卓设备上将大型位图文件调整大小以生成缩放输出文件

223

我有一个大的位图文件(比如3888x2592),现在我想将该位图调整为800x533并保存到另一个文件中。

通常情况下,我会通过调用Bitmap.createBitmap方法对位图进行缩放,但它需要一个源位图作为第一个参数,由于加载原始图像到位图对象中当然会超过内存限制(例如见这里),因此我无法提供这个参数。

我也无法使用BitmapFactory.decodeFile(file, options)读取位图文件,并提供BitmapFactory.Options.inSampleSize参数,因为我想要将其调整为精确的宽度和高度。使用inSampleSize将会将位图缩放到972x648(如果我使用inSampleSize=4)或778x518(如果我使用inSampleSize=5,这甚至不是2的幂次方)。

我也想避免在第一步中使用inSampleSize读取图像,例如以972x648的大小读取图像,然后在第二步将其调整为精确的800x533,因为与直接调整原始图像相比,质量会很差。

总结一下我的问题: 有没有一种方法可以读取一个10MP或更大的大型图像文件,并将其调整为指定的新宽度和高度,而不会出现OutOfMemory异常?

我还尝试过使用BitmapFactory.decodeFile(file, options)并手动设置Options.outHeight和Options.outWidth值为800和533,但这种方式行不通。


不,outHeight和outWidth是解码方法的out参数。话虽如此,我遇到了和你一样的问题,而且对这个两步方法也不太满意。 - rds
经常,谢天谢地,你可以使用一行代码.. https://dev59.com/Cmw05IYBdhLWcg3w_Guw#17733530 - Fattie
读者们,请注意这个非常重要的 QA!!!https://dev59.com/VWAf5IYBdhLWcg3w7mU_#24135522 - Fattie
1
请注意,这个问题现在已经五年了,完整的解决方案是.. https://dev59.com/VWAf5IYBdhLWcg3w7mU_#24135522 干杯! - Fattie
2
现在有关于这个主题的官方文档:https://developer.android.com/training/displaying-bitmaps/load-bitmap.html - Vince
21个回答

151

不. 我希望有人能纠正我,但我接受了你尝试的负载/调整大小方法作为妥协。

以下是任何人浏览时应遵循的步骤:

  1. 计算最大可能的inSampleSize,仍产生比您的目标更大的图像。
  2. 使用BitmapFactory.decodeFile(file, options)加载图像,并将inSampleSize作为选项传递。
  3. 使用Bitmap.createScaledBitmap()调整大小到所需尺寸。

我试图避免这种情况。那么没有办法直接一步调整大图片的大小吗? - Manuel
2
据我所知没有,但是不要因此而停止你进一步探索。 - Justin
好的,目前我会接受这个作为我的答案。如果我发现其他方法,我会告诉你的。 - Manuel
正如PSIXO在回答中提到的那样,如果您在使用inSampleSize后仍然遇到问题,您可能还想使用android:largeHeap。 - user276648
位图变量变为空了。 - Prasad

100

Justin的回答翻译成了代码(对我完美地起作用):

private Bitmap getBitmap(String path) {

Uri uri = getImageUri(path);
InputStream in = null;
try {
    final int IMAGE_MAX_SIZE = 1200000; // 1.2MP
    in = mContentResolver.openInputStream(uri);

    // Decode image size
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeStream(in, null, options);
    in.close();



    int scale = 1;
    while ((options.outWidth * options.outHeight) * (1 / Math.pow(scale, 2)) > 
          IMAGE_MAX_SIZE) {
       scale++;
    }
    Log.d(TAG, "scale = " + scale + ", orig-width: " + options.outWidth + ", 
       orig-height: " + options.outHeight);

    Bitmap resultBitmap = null;
    in = mContentResolver.openInputStream(uri);
    if (scale > 1) {
        scale--;
        // scale to max possible inSampleSize that still yields an image
        // larger than target
        options = new BitmapFactory.Options();
        options.inSampleSize = scale;
        resultBitmap = BitmapFactory.decodeStream(in, null, options);

        // resize to desired dimensions
        int height = resultBitmap.getHeight();
        int width = resultBitmap.getWidth();
        Log.d(TAG, "1th scale operation dimenions - width: " + width + ",
           height: " + height);

        double y = Math.sqrt(IMAGE_MAX_SIZE
                / (((double) width) / height));
        double x = (y / height) * width;

        Bitmap scaledBitmap = Bitmap.createScaledBitmap(resultBitmap, (int) x, 
           (int) y, true);
        resultBitmap.recycle();
        resultBitmap = scaledBitmap;

        System.gc();
    } else {
        resultBitmap = BitmapFactory.decodeStream(in);
    }
    in.close();

    Log.d(TAG, "bitmap size - width: " +resultBitmap.getWidth() + ", height: " + 
       resultBitmap.getHeight());
    return resultBitmap;
} catch (IOException e) {
    Log.e(TAG, e.getMessage(),e);
    return null;
}

15
使用变量名如“b”会使阅读难度加大,但是回答仍然不错。 - Oliver Dixon
@Ofir:getImageUri(path); 我需要在这个方法中传递什么? - Biginner
1
使用 (wh)>>scale 要比 (wh)/Math.pow(scale, 2) 更高效。 - david.perez
2
请不要调用 System.gc() - user964843
谢谢@Ofir,但这个转换不保留图像方向 :-/ - JoeCoolman
非常感谢,它工作得很好。顺便说一句,可以使用bitmapOptions.inJustDecodeBounds = false来解码位图,而不是使用新实例。 - Phong Nguyen

45

这是Mojo Risin和Ofir的解决方案“结合”而成的。这会给你一个在最大宽度和最大高度边界内按比例调整大小的图像。

  1. 它只读取元数据以获取原始大小(options.inJustDecodeBounds)
  2. 它使用粗略的调整大小来节省内存(itmap.createScaledBitmap)
  3. 它使用基于先前创建的粗略Bitamp的精确调整大小的图像。

对我来说,它在5兆像素以下的图片上表现良好。

try
{
    int inWidth = 0;
    int inHeight = 0;

    InputStream in = new FileInputStream(pathOfInputImage);

    // decode image size (decode metadata only, not the whole image)
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeStream(in, null, options);
    in.close();
    in = null;

    // save width and height
    inWidth = options.outWidth;
    inHeight = options.outHeight;

    // decode full image pre-resized
    in = new FileInputStream(pathOfInputImage);
    options = new BitmapFactory.Options();
    // calc rought re-size (this is no exact resize)
    options.inSampleSize = Math.max(inWidth/dstWidth, inHeight/dstHeight);
    // decode full image
    Bitmap roughBitmap = BitmapFactory.decodeStream(in, null, options);

    // calc exact destination size
    Matrix m = new Matrix();
    RectF inRect = new RectF(0, 0, roughBitmap.getWidth(), roughBitmap.getHeight());
    RectF outRect = new RectF(0, 0, dstWidth, dstHeight);
    m.setRectToRect(inRect, outRect, Matrix.ScaleToFit.CENTER);
    float[] values = new float[9];
    m.getValues(values);

    // resize bitmap
    Bitmap resizedBitmap = Bitmap.createScaledBitmap(roughBitmap, (int) (roughBitmap.getWidth() * values[0]), (int) (roughBitmap.getHeight() * values[4]), true);

    // save image
    try
    {
        FileOutputStream out = new FileOutputStream(pathOfOutputImage);
        resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 80, out);
    }
    catch (Exception e)
    {
        Log.e("Image", e.getMessage(), e);
    }
}
catch (IOException e)
{
    Log.e("Image", e.getMessage(), e);
}

24

为什么不使用API?

int h = 48; // height in pixels
int w = 48; // width in pixels    
Bitmap scaled = Bitmap.createScaledBitmap(largeBitmap, w, h, true);

23
因为这并不能解决我的问题,我的问题是:"......它需要一个源位图作为第一个参数,但我无法提供它,因为将原始图像加载到位图对象中当然会超出内存限制。"因此,我也无法将位图传递给.createScaledBitmap方法,因为我仍然需要首先将大图像加载到位图对象中。 - Manuel
2
好的,我重新阅读了你的问题,基本上(如果我理解正确)它归结为“我能否在不将原始文件加载到内存中的情况下调整图像大小?” 如果是这样-我对图像处理的复杂性了解不够,无法回答,但有些东西告诉我1.它不可从API获得,2.它不会是一行代码。 我会将其标记为收藏夹-如果您(或其他人)解决了此问题,那将很有趣。 - Bostone
它对我有用,因为我得到了 URI 并将其转换为位图,所以对它们进行缩放很容易,最简单的一种方法加1。 - Hamza

22

承认到目前为止其他优秀的回答,我看过的最好的代码在拍照工具的文档中。

请参阅标题为“解码缩放图像”的部分。

http://developer.android.com/training/camera/photobasics.html

它提出的解决方案是像这里的其他方案一样进行缩放和调节大小的解决方案,但它非常简洁。

我已经将其下面的代码复制为一个方便使用的函数。

private void setPic(String imagePath, ImageView destination) {
    int targetW = destination.getWidth();
    int targetH = destination.getHeight();
    // Get the dimensions of the bitmap
    BitmapFactory.Options bmOptions = new BitmapFactory.Options();
    bmOptions.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(imagePath, bmOptions);
    int photoW = bmOptions.outWidth;
    int photoH = bmOptions.outHeight;

    // Determine how much to scale down the image
    int scaleFactor = Math.min(photoW/targetW, photoH/targetH);

    // Decode the image file into a Bitmap sized to fill the View
    bmOptions.inJustDecodeBounds = false;
    bmOptions.inSampleSize = scaleFactor;
    bmOptions.inPurgeable = true;

    Bitmap bitmap = BitmapFactory.decodeFile(imagePath, bmOptions);
    destination.setImageBitmap(bitmap);
}

1
首先,你正在除以整数,这将向下取整结果。其次,代码会崩溃,因为targetW或targetH为0(尽管我知道这没有多大意义)。第三,inSampleSize应该是2的幂。 - cybergen
不要误会。这肯定会加载一张图片,但如果意图将ints向下取整,它看起来并不是这样。而且这绝对不是正确的答案,因为该图像不会按预期缩放。除非图像视图的大小小于或等于图像的一半,否则它不会执行任何操作。然后,在图像视图的大小为图像大小的1/4时,仍然不会发生任何事情。以此类推,按二次幂进行操作! - cybergen

19

阅读这些答案并查看Android文档后,以下是将位图调整大小而无需将其加载到内存中的代码:

public Bitmap getResizedBitmap(int targetW, int targetH,  String imagePath) {

    // Get the dimensions of the bitmap
    BitmapFactory.Options bmOptions = new BitmapFactory.Options();
    //inJustDecodeBounds = true <-- will not load the bitmap into memory
    bmOptions.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(imagePath, bmOptions);
    int photoW = bmOptions.outWidth;
    int photoH = bmOptions.outHeight;

    // Determine how much to scale down the image
    int scaleFactor = Math.min(photoW/targetW, photoH/targetH);

    // Decode the image file into a Bitmap sized to fill the View
    bmOptions.inJustDecodeBounds = false;
    bmOptions.inSampleSize = scaleFactor;
    bmOptions.inPurgeable = true;

    Bitmap bitmap = BitmapFactory.decodeFile(imagePath, bmOptions);
    return(bitmap);
}

请注意,bmOptions.inPurgeable = true; 已被弃用。 - Ravit
inSampleSize 需要是2的幂次方。 - thumbmunkeys

6
当我有大型位图并且想要调整大小时,我使用以下方法进行解码:
BitmapFactory.Options options = new BitmapFactory.Options();
InputStream is = null;
is = new FileInputStream(path_to_file);
BitmapFactory.decodeStream(is,null,options);
is.close();
is = new FileInputStream(path_to_file);
// here w and h are the desired width and height
options.inSampleSize = Math.max(options.outWidth/w, options.outHeight/h);
// bitmap is the resized bitmap
Bitmap bitmap = BitmapFactory.decodeStream(is,null,options);

1
由于inSampleSize是一个整数,你很少会得到你想要的确切像素宽度和高度。有时候你可能会接近,但也可能会相差甚远,这取决于小数点位数。 - Manuel
早上好,我尝试了你在这个帖子中发布的代码,但似乎不起作用,我做错了什么吗?欢迎任何建议 :-) - RRTW

5
这可能对查看此问题的其他人有用。我重写了Justin的代码,以使该方法可以接收所需的目标大小对象。在使用画布时,这非常有效。所有功劳应归功于JUSTIN的伟大初始代码。
    private Bitmap getBitmap(int path, Canvas canvas) {

        Resources resource = null;
        try {
            final int IMAGE_MAX_SIZE = 1200000; // 1.2MP
            resource = getResources();

            // Decode image size
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeResource(resource, path, options);

            int scale = 1;
            while ((options.outWidth * options.outHeight) * (1 / Math.pow(scale, 2)) > 
                  IMAGE_MAX_SIZE) {
               scale++;
            }
            Log.d("TAG", "scale = " + scale + ", orig-width: " + options.outWidth + ", orig-height: " + options.outHeight);

            Bitmap pic = null;
            if (scale > 1) {
                scale--;
                // scale to max possible inSampleSize that still yields an image
                // larger than target
                options = new BitmapFactory.Options();
                options.inSampleSize = scale;
                pic = BitmapFactory.decodeResource(resource, path, options);

                // resize to desired dimensions
                int height = canvas.getHeight();
                int width = canvas.getWidth();
                Log.d("TAG", "1th scale operation dimenions - width: " + width + ", height: " + height);

                double y = Math.sqrt(IMAGE_MAX_SIZE
                        / (((double) width) / height));
                double x = (y / height) * width;

                Bitmap scaledBitmap = Bitmap.createScaledBitmap(pic, (int) x, (int) y, true);
                pic.recycle();
                pic = scaledBitmap;

                System.gc();
            } else {
                pic = BitmapFactory.decodeResource(resource, path);
            }

            Log.d("TAG", "bitmap size - width: " +pic.getWidth() + ", height: " + pic.getHeight());
            return pic;
        } catch (Exception e) {
            Log.e("TAG", e.getMessage(),e);
            return null;
        }
    }

Justin的代码非常有效地减少了处理大位图时的开销。


4
我不确定我的解决方案是否符合最佳实践,但我通过使用inDensityinTargetDensity选项来实现按照需要缩放加载位图。当不加载可绘制资源时,inDensity初始值为0,因此此方法适用于加载非资源图片。

imageUrimaxImageSideLengthcontext 变量是我的方法参数。出于清晰起见,我仅发布了该方法实现而没有包装AsyncTask。

            ContentResolver resolver = context.getContentResolver();
            InputStream is;
            try {
                is = resolver.openInputStream(imageUri);
            } catch (FileNotFoundException e) {
                Log.e(TAG, "Image not found.", e);
                return null;
            }
            Options opts = new Options();
            opts.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(is, null, opts);

            // scale the image
            float maxSideLength = maxImageSideLength;
            float scaleFactor = Math.min(maxSideLength / opts.outWidth, maxSideLength / opts.outHeight);
            // do not upscale!
            if (scaleFactor < 1) {
                opts.inDensity = 10000;
                opts.inTargetDensity = (int) ((float) opts.inDensity * scaleFactor);
            }
            opts.inJustDecodeBounds = false;

            try {
                is.close();
            } catch (IOException e) {
                // ignore
            }
            try {
                is = resolver.openInputStream(imageUri);
            } catch (FileNotFoundException e) {
                Log.e(TAG, "Image not found.", e);
                return null;
            }
            Bitmap bitmap = BitmapFactory.decodeStream(is, null, opts);
            try {
                is.close();
            } catch (IOException e) {
                // ignore
            }

            return bitmap;

2
非常好!使用inDensity而不是Bitmap.createScaledBitmap节省了大量内存堆。结合inSampleSize效果更好。 - Ostkontentitan

2
我使用了以下代码:

我使用了这样的代码:

  String filePath=Environment.getExternalStorageDirectory()+"/test_image.jpg";
  BitmapFactory.Options options=new BitmapFactory.Options();
  InputStream is=new FileInputStream(filePath);
  BitmapFactory.decodeStream(is, null, options);
  is.close();
  is=new FileInputStream(filePath);
  // here w and h are the desired width and height
  options.inSampleSize=Math.max(options.outWidth/460, options.outHeight/288); //Max 460 x 288 is my desired...
  // bmp is the resized bitmap
  Bitmap bmp=BitmapFactory.decodeStream(is, null, options);
  is.close();
  Log.d(Constants.TAG, "Scaled bitmap bytes, "+bmp.getRowBytes()+", width:"+bmp.getWidth()+", height:"+bmp.getHeight());

我尝试使用原始图片,大小为1230 x 1230,但位图显示大小为330 x 330。
如果尝试使用2590 x 3849的图片,则会出现OutOfMemoryError错误。

我追踪了它,如果原始位图太大,仍然会在"BitmapFactory.decodeStream(is, null, options);"这一行抛出OutOfMemoryError错误...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接