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

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个回答

2
上述代码稍微简洁了一些。为了确保InputStreams也能关闭,进行了finally close包装:
*注意: 输入:InputStream is,int w,int h 输出:位图
    try
    {

        final int inWidth;
        final int inHeight;

        final File tempFile = new File(temp, System.currentTimeMillis() + is.toString() + ".temp");

        {

            final FileOutputStream tempOut = new FileOutputStream(tempFile);

            StreamUtil.copyTo(is, tempOut);

            tempOut.close();

        }



        {

            final InputStream in = new FileInputStream(tempFile);
            final BitmapFactory.Options options = new BitmapFactory.Options();

            try {

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

            }
            finally {
                in.close();
            }

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

        }

        final Bitmap roughBitmap;

        {

            // decode full image pre-resized
            final InputStream in = new FileInputStream(tempFile);

            try {

                final BitmapFactory.Options options = new BitmapFactory.Options();
                // calc rought re-size (this is no exact resize)
                options.inSampleSize = Math.max(inWidth/w, inHeight/h);
                // decode full image
                roughBitmap = BitmapFactory.decodeStream(in, null, options);

            }
            finally {
                in.close();
            }

            tempFile.delete();

        }

        float[] values = new float[9];

        {

            // calc exact destination size
            Matrix m = new Matrix();
            RectF inRect = new RectF(0, 0, roughBitmap.getWidth(), roughBitmap.getHeight());
            RectF outRect = new RectF(0, 0, w, h);
            m.setRectToRect(inRect, outRect, Matrix.ScaleToFit.CENTER);
            m.getValues(values);

        }

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

        return resizedBitmap;

    }
    catch (IOException e) {

        logger.error("Error:" , e);
        throw new ResourceException("could not create bitmap");

    }

2
考虑到您想要精确调整大小并保持尽可能高的质量,我认为您应该尝试以下方法:
  1. 使用 BitmapFactory.decodeFile 并提供 checkSizeOptions.inJustDecodeBounds 参数来找出调整大小后图像的大小。
  2. 计算您的设备上可以使用的最大 inSampleSize(采样率),以避免超出内存。bitmapSizeInBytes = 2*width*height; 对于您的图片,通常 inSampleSize=2 就足够了,因为您只需要 2*1944x1296)=4.8Mb 的内存。
  3. 使用带有 inSampleSize 参数的 BitmapFactory.decodeFile 加载位图。
  4. 将位图缩放到精确的大小。
动机:多步缩放可以给您更高质量的图片,但是无法保证比使用高 inSampleSize 更好。实际上,我认为您也可以使用 inSampleSize=5(不是 2 的幂)直接进行缩放操作。或者只需使用 inSampleSize=4,然后您就可以在 UI 中使用该图像。如果您将其发送到服务器,则可以在服务器端对其进行精确大小的缩放,从而允许您使用高级缩放技术。
注:如果在第三步加载的位图至少大于目标宽度的4倍(即4 * targetWidth < width),则您可能可以使用多次调整大小来实现更好的质量。至少在通用Java中有效,在Android中,您无法指定用于缩放的插值选项。 http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html

1

1
如果您绝对想要进行一步调整大小,您可能可以加载整个位图,如果android:largeHeap = true,但是正如您所看到的,这并不是一个好建议。
从文档中: android:largeHeap 您的应用程序进程是否应该使用大型Dalvik堆。这适用于为应用程序创建的所有进程。它仅适用于加载到进程中的第一个应用程序;如果您使用共享用户ID允许多个应用程序使用进程,则它们都必须始终使用此选项,否则将产生不可预测的结果。 大多数应用程序不需要此功能,而应该专注于减少其整体内存使用以提高性能。启用此功能也不能保证可用内存固定增加,因为某些设备受其总可用内存的限制。

1
为了以“正确”的方式缩放图像,而不跳过任何像素,您必须钩入图像解码器以逐行执行下采样。Android(以及其基础的Skia库)没有提供此类钩子,因此您必须自己编写。假设您正在处理jpeg图像,则最好直接使用C中的libjpeg。
考虑到涉及的复杂性,对于图像预览类型的应用程序,使用两步子采样然后重新缩放可能是最好的选择。

0
 Bitmap yourBitmap;
 Bitmap resized = Bitmap.createScaledBitmap(yourBitmap, newWidth, newHeight, true);

或者:

 resized = Bitmap.createScaledBitmap(yourBitmap,(int)(yourBitmap.getWidth()*0.8), (int)(yourBitmap.getHeight()*0.8), true);

0

这是我使用的代码,在Android上内存中解码大图像没有任何问题。只要我的输入参数大约为1024x1024,我就能够解码大于20MB的图像。您可以将返回的位图保存到另一个文件中。在此方法下面是另一种方法,我也用它来缩放图像到新的位图。随意使用此代码。

/*****************************************************************************
 * public decode - decode the image into a Bitmap
 * 
 * @param xyDimension
 *            - The max XY Dimension before the image is scaled down - XY =
 *            1080x1080 and Image = 2000x2000 image will be scaled down to a
 *            value equal or less then set value.
 * @param bitmapConfig
 *            - Bitmap.Config Valid values = ( Bitmap.Config.ARGB_4444,
 *            Bitmap.Config.RGB_565, Bitmap.Config.ARGB_8888 )
 * 
 * @return Bitmap - Image - a value of "null" if there is an issue decoding
 *         image dimension
 * 
 * @throws FileNotFoundException
 *             - If the image has been removed while this operation is
 *             taking place
 */
public Bitmap decode( int xyDimension, Bitmap.Config bitmapConfig ) throws FileNotFoundException
{
    // The Bitmap to return given a Uri to a file
    Bitmap bitmap = null;
    File file = null;
    FileInputStream fis = null;
    InputStream in = null;

    // Try to decode the Uri
    try
    {
        // Initialize scale to no real scaling factor
        double scale = 1;

        // Get FileInputStream to get a FileDescriptor
        file = new File( this.imageUri.getPath() );

        fis = new FileInputStream( file );
        FileDescriptor fd = fis.getFD();

        // Get a BitmapFactory Options object
        BitmapFactory.Options o = new BitmapFactory.Options();

        // Decode only the image size
        o.inJustDecodeBounds = true;
        o.inPreferredConfig = bitmapConfig;

        // Decode to get Width & Height of image only
        BitmapFactory.decodeFileDescriptor( fd, null, o );
        BitmapFactory.decodeStream( null );

        if( o.outHeight > xyDimension || o.outWidth > xyDimension )
        {
            // Change the scale if the image is larger then desired image
            // max size
            scale = Math.pow( 2, (int) Math.round( Math.log( xyDimension / (double) Math.max( o.outHeight, o.outWidth ) ) / Math.log( 0.5 ) ) );
        }

        // Decode with inSampleSize scale will either be 1 or calculated value
        o.inJustDecodeBounds = false;
        o.inSampleSize = (int) scale;

        // Decode the Uri for real with the inSampleSize
        in = new BufferedInputStream( fis );
        bitmap = BitmapFactory.decodeStream( in, null, o );
    }
    catch( OutOfMemoryError e )
    {
        Log.e( DEBUG_TAG, "decode : OutOfMemoryError" );
        e.printStackTrace();
    }
    catch( NullPointerException e )
    {
        Log.e( DEBUG_TAG, "decode : NullPointerException" );
        e.printStackTrace();
    }
    catch( RuntimeException e )
    {
        Log.e( DEBUG_TAG, "decode : RuntimeException" );
        e.printStackTrace();
    }
    catch( FileNotFoundException e )
    {
        Log.e( DEBUG_TAG, "decode : FileNotFoundException" );
        e.printStackTrace();
    }
    catch( IOException e )
    {
        Log.e( DEBUG_TAG, "decode : IOException" );
        e.printStackTrace();
    }

    // Save memory
    file = null;
    fis = null;
    in = null;

    return bitmap;

} // decode

注意:除了createScaledBitmap调用上面的decode方法之外,这些方法彼此没有任何关联。请注意,宽度和高度可能会与原始图像不同。

/*****************************************************************************
 * public createScaledBitmap - Creates a new bitmap, scaled from an existing
 * bitmap.
 * 
 * @param dstWidth
 *            - Scale the width to this dimension
 * @param dstHeight
 *            - Scale the height to this dimension
 * @param xyDimension
 *            - The max XY Dimension before the original image is scaled
 *            down - XY = 1080x1080 and Image = 2000x2000 image will be
 *            scaled down to a value equal or less then set value.
 * @param bitmapConfig
 *            - Bitmap.Config Valid values = ( Bitmap.Config.ARGB_4444,
 *            Bitmap.Config.RGB_565, Bitmap.Config.ARGB_8888 )
 * 
 * @return Bitmap - Image scaled - a value of "null" if there is an issue
 * 
 */
public Bitmap createScaledBitmap( int dstWidth, int dstHeight, int xyDimension, Bitmap.Config bitmapConfig )
{
    Bitmap scaledBitmap = null;

    try
    {
        Bitmap bitmap = this.decode( xyDimension, bitmapConfig );

        // Create an empty Bitmap which will contain the new scaled bitmap
        // This scaled bitmap should be the size we want to scale the
        // original bitmap too
        scaledBitmap = Bitmap.createBitmap( dstWidth, dstHeight, bitmapConfig );

        float ratioX = dstWidth / (float) bitmap.getWidth();
        float ratioY = dstHeight / (float) bitmap.getHeight();
        float middleX = dstWidth / 2.0f;
        float middleY = dstHeight / 2.0f;

        // Used to for scaling the image
        Matrix scaleMatrix = new Matrix();
        scaleMatrix.setScale( ratioX, ratioY, middleX, middleY );

        // Used to do the work of scaling
        Canvas canvas = new Canvas( scaledBitmap );
        canvas.setMatrix( scaleMatrix );
        canvas.drawBitmap( bitmap, middleX - bitmap.getWidth() / 2, middleY - bitmap.getHeight() / 2, new Paint( Paint.FILTER_BITMAP_FLAG ) );
    }
    catch( IllegalArgumentException e )
    {
        Log.e( DEBUG_TAG, "createScaledBitmap : IllegalArgumentException" );
        e.printStackTrace();
    }
    catch( NullPointerException e )
    {
        Log.e( DEBUG_TAG, "createScaledBitmap : NullPointerException" );
        e.printStackTrace();
    }
    catch( FileNotFoundException e )
    {
        Log.e( DEBUG_TAG, "createScaledBitmap : FileNotFoundException" );
        e.printStackTrace();
    }

    return scaledBitmap;
} // End createScaledBitmap

这里的比例尺功率计算是错误的;只需使用Android文档页面上的计算即可。 - Fattie

0
这对我有用。该函数获取SD卡上文件的路径并返回最大可显示大小的位图。 代码来自Ofir,进行了一些更改,如将图像文件存储在SD卡上而不是资源中,并且宽度和高度从Display对象中获取。
private Bitmap makeBitmap(String path) {

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

        // Decode image size
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(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.decodeFile(path, options);

            // resize to desired dimensions

            Display display = getWindowManager().getDefaultDisplay();
            Point size = new Point();
            display.getSize(size);
            int width = size.y;
            int height = size.x;

            //int height = imageView.getHeight();
            //int width = imageView.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.decodeFile(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;
    }

}

0

0

我使用 Integer.numberOfLeadingZeros 来计算最佳样本大小,以获得更好的性能。

Kotlin 的完整代码:

@Throws(IOException::class)
fun File.decodeBitmap(options: BitmapFactory.Options): Bitmap? {
    return inputStream().use {
        BitmapFactory.decodeStream(it, null, options)
    }
}

@Throws(IOException::class)
fun File.decodeBitmapAtLeast(
        @androidx.annotation.IntRange(from = 1) width: Int,
        @androidx.annotation.IntRange(from = 1) height: Int
): Bitmap? {
    val options = BitmapFactory.Options()

    options.inJustDecodeBounds = true
    decodeBitmap(options)

    val ow = options.outWidth
    val oh = options.outHeight

    if (ow == -1 || oh == -1) return null

    val w = ow / width
    val h = oh / height

    if (w > 1 && h > 1) {
        val p = 31 - maxOf(Integer.numberOfLeadingZeros(w), Integer.numberOfLeadingZeros(h))
        options.inSampleSize = 1 shl maxOf(0, p)
    }
    options.inJustDecodeBounds = false
    return decodeBitmap(options)
}

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