保持宽高比例的缩放位图

81

我希望将一个Bitmap按照运行时的宽度和高度进行缩放,保持纵横比并使Bitmap填充整个宽度,并使图像垂直居中,可以通过裁剪多余部分或填充0 alpha像素来填补空隙。

我目前是通过创建所有alpha为0像素的Bitmap并在其上绘制图像Bitmap来重绘位图,缩放到精确指定的宽度并保持纵横比,但是它最终会丢失/损坏像素数据。

以下是我的操作方式:

Bitmap background = Bitmap.createBitmap((int)width, (int)height, Config.ARGB_8888);
float originalWidth = originalImage.getWidth(), originalHeight = originalImage.getHeight();
Canvas canvas = new Canvas(background);
float scale = width/originalWidth;
float xTranslation = 0.0f, yTranslation = (height - originalHeight * scale)/2.0f;
Matrix transformation = new Matrix();
transformation.postTranslate(xTranslation, yTranslation);
transformation.preScale(scale, scale);
canvas.drawBitmap(originalImage, transformation, null);
return background;

有没有可以更好地完成这项任务的库或代码呢?我希望图像尽可能清晰,但我知道我的函数效果不太好。

我知道我可以使用整数缩放来保持图像的清晰度,而不是浮点缩放,但我需要宽度填充满100%。

此外,我知道ImageViewGravity.CENTER_CROP功能,但它也使用整数缩放,因此在不应该裁剪的情况下也会切断图像的宽度。

13个回答

132

这将尊重最大宽度和最大高度,这意味着生成的位图永远不会超过这些尺寸:

 private static Bitmap resize(Bitmap image, int maxWidth, int maxHeight) {
    if (maxHeight > 0 && maxWidth > 0) {
        int width = image.getWidth();
        int height = image.getHeight();
        float ratioBitmap = (float) width / (float) height;
        float ratioMax = (float) maxWidth / (float) maxHeight;

        int finalWidth = maxWidth;
        int finalHeight = maxHeight;
        if (ratioMax > ratioBitmap) {
            finalWidth = (int) ((float)maxHeight * ratioBitmap);
        } else {
            finalHeight = (int) ((float)maxWidth / ratioBitmap);
        }
        image = Bitmap.createScaledBitmap(image, finalWidth, finalHeight, true);
        return image;
    } else {
        return image;
    }
}

解决方案的答案是裁剪图像,但这个解决方案很完美地工作。 - Arslan Anwar
@Arslan 我用这种方法没有进行任何裁剪。你能再详细解释一下吗? - Joshua Pinter
11
这个问题的被接受答案导致了裁剪。这个答案对我非常有效,因为它不会导致裁剪。 - Jake Lee
如果我的宽度和高度为1200x1200,而我的最大宽度和高度为700x700,则rationBitmap和rationMax都为0.0,这是一个有效的条件吗?因为之后会出现除以零异常。 - Kishan Donga
perfect to me :) - byzz
@KishanDonga 在这种情况下比率是1,而不是0...实际上,比率是通过将宽度除以高度获得的,因此它永远不可能为0(除非宽度为0,这是没有意义的)。 - Fran Marzoa

76

这个怎么样:

Bitmap background = Bitmap.createBitmap((int)width, (int)height, Config.ARGB_8888);

float originalWidth = originalImage.getWidth(); 
float originalHeight = originalImage.getHeight();

Canvas canvas = new Canvas(background);

float scale = width / originalWidth;

float xTranslation = 0.0f;
float yTranslation = (height - originalHeight * scale) / 2.0f;

Matrix transformation = new Matrix();
transformation.postTranslate(xTranslation, yTranslation);
transformation.preScale(scale, scale);

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

canvas.drawBitmap(originalImage, transformation, paint);

return background;

我添加了一个paint来过滤缩放后的位图。


好的,比例尺不能改变,因为我需要填充宽度,而且也不能裁剪宽度。不过,绘画工具解决了问题。谢谢!你能否编辑答案以切换回我的缩放? - RileyE
9
但这是裁剪图像。 - Shahjahan Khan
这就是它!终于完成了。 - GabrielBB
它将横向或纵向的图像裁剪成正方形以适应尺寸。 - CrackerKSR

33

这里是我 Utils 类中的一个方法,可以完成该任务:

public static Bitmap scaleBitmapAndKeepRation(Bitmap targetBmp,int reqHeightInPixels,int reqWidthInPixels)
    {
        Matrix matrix = new Matrix();
        matrix .setRectToRect(new RectF(0, 0, targetBmp.getWidth(), targetBmp.getHeight()), new RectF(0, 0, reqWidthInPixels, reqHeightInPixels), Matrix.ScaleToFit.CENTER);
        Bitmap scaledBitmap = Bitmap.createBitmap(targetBmp, 0, 0, targetBmp.getWidth(), targetBmp.getHeight(), matrix, true);
        return scaledBitmap;
    }

6
请注意,这仅适用于API 17(果冻豆)或更高版本。 - RileyE
3
整洁!!这很简单。除了变量命名规范之外。最好将TargetBmp更改为targetBmp。适用于纵向横向图像。 - Ismail Iqbal
使用这种方法缩放位图时,位图质量会降低。对我没有起作用。请给我另一个解决方案。 - Rishav Singla
@RishavSingla,您正在请求解决调整图像固有成本的方案。它不是矢量图。调整大小是一种有损操作。 - Abandoned Cart

26

我这里有一个经过测试的解决方案,可以将位图文件创建为缩放后的位图:

    int scaleSize =1024;

    public Bitmap resizeImageForImageView(Bitmap bitmap) {
        Bitmap resizedBitmap = null;
        int originalWidth = bitmap.getWidth();
        int originalHeight = bitmap.getHeight();
        int newWidth = -1;
        int newHeight = -1;
        float multFactor = -1.0F;
        if(originalHeight > originalWidth) {
            newHeight = scaleSize ;
            multFactor = (float) originalWidth/(float) originalHeight;
            newWidth = (int) (newHeight*multFactor);
        } else if(originalWidth > originalHeight) {
            newWidth = scaleSize ;
            multFactor = (float) originalHeight/ (float)originalWidth;
            newHeight = (int) (newWidth*multFactor);
        } else if(originalHeight == originalWidth) {
            newHeight = scaleSize ;
            newWidth = scaleSize ;
        }
        resizedBitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, false);
        return resizedBitmap;
    }

请注意,我需要缩放过的位图,其最大尺寸为4096x4096像素,但缩放时需要保留纵横比。如果您需要宽度或高度的其他值,请替换“4096”这些值。

这只是对Coen答案的补充,但他代码中的问题在于计算比率的那一行。将两个整数除以二会得到一个整数,如果结果小于1,则会四舍五入为0。因此,这会引发“除以零”的异常。


1
我建议将新的尺寸值提取到变量或参数中,以避免重复。此外,您可以使用新的尺寸值初始化newWidth和newHeight,并仅重新设置更改的变量以保留比例(也不需要预先初始化multFactor或resizedBitmap)。 - AlexGuti
1
你说得没错,@Christopher Reichel,上面的代码并没有缩放所有的尺寸,只是那些宽度大于高度的。这个答案更好,但是@joaomgcd的答案比这个更好,因为它进行了代码优化和方法参数的调整。 - Bahadir Tasdemir
它是否会拉伸图像?如果我想将图像调整大小以适应正方形并保持纵横比,期望留下未使用的透明区域。 - CrackerKSR

14

以上的回答都对我没用,我刚刚创建了一种方法,通过将空白区域涂黑来设置所有维度为所需尺寸。这是我的方法:

/**
 * Scale the image preserving the ratio
 * @param imageToScale Image to be scaled
 * @param destinationWidth Destination width after scaling
 * @param destinationHeight Destination height after scaling
 * @return New scaled bitmap preserving the ratio
 */
public static Bitmap scalePreserveRatio(Bitmap imageToScale, int destinationWidth,
        int destinationHeight) {
    if (destinationHeight > 0 && destinationWidth > 0 && imageToScale != null) {
        int width = imageToScale.getWidth();
        int height = imageToScale.getHeight();

        //Calculate the max changing amount and decide which dimension to use
        float widthRatio = (float) destinationWidth / (float) width;
        float heightRatio = (float) destinationHeight / (float) height;

        //Use the ratio that will fit the image into the desired sizes
        int finalWidth = (int)Math.floor(width * widthRatio);
        int finalHeight = (int)Math.floor(height * widthRatio);
        if (finalWidth > destinationWidth || finalHeight > destinationHeight) {
            finalWidth = (int)Math.floor(width * heightRatio);
            finalHeight = (int)Math.floor(height * heightRatio);
        }

        //Scale given bitmap to fit into the desired area
        imageToScale = Bitmap.createScaledBitmap(imageToScale, finalWidth, finalHeight, true);

        //Created a bitmap with desired sizes
        Bitmap scaledImage = Bitmap.createBitmap(destinationWidth, destinationHeight, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(scaledImage);

        //Draw background color
        Paint paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), paint);

        //Calculate the ratios and decide which part will have empty areas (width or height)
        float ratioBitmap = (float)finalWidth / (float)finalHeight;
        float destinationRatio = (float) destinationWidth / (float) destinationHeight;
        float left = ratioBitmap >= destinationRatio ? 0 : (float)(destinationWidth - finalWidth) / 2;
        float top = ratioBitmap < destinationRatio ? 0: (float)(destinationHeight - finalHeight) / 2;
        canvas.drawBitmap(imageToScale, left, top, null);

        return scaledImage;
    } else {
        return imageToScale;
    }
}
例如:假设您有一张100 x 100的图片,但期望大小为300 x 50,则此方法将把您的图片转换为50 x 50并绘制到一个具有300 x 50尺寸的新图片中(空白区域将会是黑色)。
另一个例子:假设您有一张600 x 1000的图片,期望大小再次为300 x 50,则您的图片将被转换为30 x 50并绘制到一个新创建的尺寸为300 x 50的图片中。
我认为这就是应该是的,Rs。

完美的答案适用于我的用例。 - Raghav Satyadev
哇,终于找到了...这正是我拼命寻找的。顺便说一下,我使用了Color.TRANSPARENT来实现透明度。 - CrackerKSR
1
你服务得很好 :) @CrackerKSR - Bahadir Tasdemir
@bahadir-tasdemir 这可能与话题无关,但您能否建议一些方法将这些帧编码为动态webp格式。 在尝试了几个图像库之后,我正在寻找动态webp编码器。 - CrackerKSR
这是一个我非常不熟悉的话题,很抱歉我不知道,我必须像你一样进行调查。 - Bahadir Tasdemir
显示剩余2条评论

9
更简单的解决方案:请注意我们将宽度设置为500像素。
 public void scaleImageKeepAspectRatio()
    {
        int imageWidth = scaledGalleryBitmap.getWidth();
        int imageHeight = scaledGalleryBitmap.getHeight();
        int newHeight = (imageHeight * 500)/imageWidth;
        scaledGalleryBitmap = Bitmap.createScaledBitmap(scaledGalleryBitmap, 500, newHeight, false);

    }

8
这也可以通过自己计算比率来完成,像这样。
private Bitmap scaleBitmap(Bitmap bm) {
    int width = bm.getWidth();
    int height = bm.getHeight();

    Log.v("Pictures", "Width and height are " + width + "--" + height);

    if (width > height) {
        // landscape
        int ratio = width / maxWidth;
        width = maxWidth;
        height = height / ratio;
    } else if (height > width) {
        // portrait
        int ratio = height / maxHeight;
        height = maxHeight;
        width = width / ratio;
    } else {
        // square
        height = maxHeight;
        width = maxWidth;
    }

    Log.v("Pictures", "after scaling Width and height are " + width + "--" + height);

    bm = Bitmap.createScaledBitmap(bm, width, height, true);
    return bm;
}

1
如果你缩小比例,比例为0,它会抛出一个除以0的异常。 - Julien D
8
没错,最重要的是解决方案对吧?这里总有些鹫嫌弃别人的想法...哎 :-) - Coen Damen

5

基于joaomgcd的答案的Kotlin扩展函数版本

private fun Bitmap.resize(maxWidth: Int, maxHeight: Int): Bitmap {
    return if (maxHeight > 0 && maxWidth > 0) {
        val width = this.width
        val height = this.height
        val ratioBitmap = width.toFloat() / height.toFloat()
        val ratioMax = maxWidth.toFloat() / maxHeight.toFloat()
        var finalWidth = maxWidth
        var finalHeight = maxHeight
        if (ratioMax > ratioBitmap) {
            finalWidth = (maxHeight.toFloat() * ratioBitmap).toInt()
        } else {
            finalHeight = (maxWidth.toFloat() / ratioBitmap).toInt()
        }
        Bitmap.createScaledBitmap(this, finalWidth, finalHeight, true)
    } else this
}

3

在Gowrav的回答中添加了RESIZE_CROP。

   enum RequestSizeOptions {
    RESIZE_FIT,
    RESIZE_INSIDE,
    RESIZE_EXACT,
    RESIZE_CENTRE_CROP
}
static Bitmap resizeBitmap(Bitmap bitmap, int reqWidth, int reqHeight, RequestSizeOptions options) {
    try {
        if (reqWidth > 0 && reqHeight > 0 && (options == RequestSizeOptions.RESIZE_FIT ||
                options == RequestSizeOptions.RESIZE_INSIDE ||
                options == RequestSizeOptions.RESIZE_EXACT || options == RequestSizeOptions.RESIZE_CENTRE_CROP)) {

            Bitmap resized = null;
            if (options == RequestSizeOptions.RESIZE_EXACT) {
                resized = Bitmap.createScaledBitmap(bitmap, reqWidth, reqHeight, false);
            } else {
                int width = bitmap.getWidth();
                int height = bitmap.getHeight();
                float scale = Math.max(width / (float) reqWidth, height / (float) reqHeight);
                if (scale > 1 || options == RequestSizeOptions.RESIZE_FIT) {
                    resized = Bitmap.createScaledBitmap(bitmap, (int) (width / scale), (int) (height / scale), false);
                }
                if (scale > 1 || options == RequestSizeOptions.RESIZE_CENTRE_CROP) {
                    int smaller_side = (height-width)>0?width:height;
                    int half_smaller_side = smaller_side/2;
                    Rect initialRect = new Rect(0,0,width,height);
                    Rect finalRect = new Rect(initialRect.centerX()-half_smaller_side,initialRect.centerY()-half_smaller_side,
                            initialRect.centerX()+half_smaller_side,initialRect.centerY()+half_smaller_side);
                    bitmap = Bitmap.createBitmap(bitmap,  finalRect.left, finalRect.top, finalRect.width(), finalRect.height(), null, true);
                    //keep in mind we have square as request for cropping, otherwise - it is useless
                    resized = Bitmap.createScaledBitmap(bitmap, reqWidth, reqHeight, false);
                }

            }
            if (resized != null) {
                if (resized != bitmap) {
                    bitmap.recycle();
                }
                return resized;
            }
        }
    } catch (Exception e) {
        Log.w("AIC", "Failed to resize cropped image, return bitmap before resize", e);
    }
    return bitmap;
}

谢谢,它提供了临时解决方案。RESIZE_INSIDE可以解决我的问题,但是在代码片段中缺少它。 - CrackerKSR

2
这是一个来自ArthurHub的很棒的库,可以在编程和交互式方面处理图像裁剪,如果你不想重新发明轮子。但是,如果你像我一样更喜欢一个非臃肿的版本...,这里展示的内部函数是一种相当复杂的方法,可以使用几个标准选项执行图像缩放。
/**
 * Resize the given bitmap to the given width/height by the given option.<br>
 */

enum RequestSizeOptions {
    RESIZE_FIT,
    RESIZE_INSIDE,
    RESIZE_EXACT
}

static Bitmap resizeBitmap(Bitmap bitmap, int reqWidth, int reqHeight, RequestSizeOptions options) {
    try {
        if (reqWidth > 0 && reqHeight > 0 && (options == RequestSizeOptions.RESIZE_FIT ||
                options == RequestSizeOptions.RESIZE_INSIDE ||
                options == RequestSizeOptions.RESIZE_EXACT)) {

            Bitmap resized = null;
            if (options == RequestSizeOptions.RESIZE_EXACT) {
                resized = Bitmap.createScaledBitmap(bitmap, reqWidth, reqHeight, false);
            } else {
                int width = bitmap.getWidth();
                int height = bitmap.getHeight();
                float scale = Math.max(width / (float) reqWidth, height / (float) reqHeight);
                if (scale > 1 || options == RequestSizeOptions.RESIZE_FIT) {
                    resized = Bitmap.createScaledBitmap(bitmap, (int) (width / scale), (int) (height / scale), false);
                }
            }
            if (resized != null) {
                if (resized != bitmap) {
                    bitmap.recycle();
                }
                return resized;
            }
        }
    } catch (Exception e) {
        Log.w("AIC", "Failed to resize cropped image, return bitmap before resize", e);
    }
    return bitmap;
}

如何实现“内部调整”功能 - CrackerKSR

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