调整图像大小以适应边界框

41

这是一个简单的问题,但今天出于某些原因我就是想不出来。

我需要将一张图片缩放到最大可能适合边界框的大小,同时保持纵横比。

基本上我正在寻找代码来填写这个函数:

void CalcNewDimensions(ref int w, ref int h, int MaxWidth, int MaxHeight);

其中w和h是原始高度和宽度(单位为英寸),新的高度和宽度为out,MaxWidth和MaxHeight定义了图像必须适合的边界框。


8
请勿滥用引用。更好的方法是创建一个不可变的 Rectangle 结构体,其中有一个宽度和一个高度字段,然后编写一个 ExpandToBound 方法,该方法接受两个Rectangle并返回结果Rectangle。当你将函数实现为函数时,对函数进行推理会更容易。参数进入,结果出来;函数不会改变它们不拥有的状态。 - Eric Lippert
@Eric Lippert - 同意,这个例子并不是我实际实现的函数,只是一个精简版本,避免将矩形结构体或其他与问题核心无关的东西混淆。 - Eric Petroelje
9个回答

98

找出哪个更小:MaxWidth / w 或者 MaxHeight / h,然后将wh乘以这个数。

说明:

你需要找到使图像适应的缩放因子。

要找到宽度的缩放因子s,那么s必须满足:s * w = MaxWidth。因此,缩放因子是MaxWidth / w

高度同理。

需要最大缩放的那个(较小的s)是你必须将整个图像缩放的因子。


如果你使用浮点数进行计算,你可能会发现与边界框匹配的维度略有偏差(有时是不可预测的)。当你确定哪个维度不匹配时,最好假设另一个维度正好符合要求。 - Marcus Downing
6
三年后,我在谷歌搜索结果的顶部。没有繁琐的过程,也没有麻烦。谢谢!(+1指的是点赞) - chaosTechnician
3
就像你的回复不是用 C 写的一样,所以我可以将它应用到 Python 上。 - Luke Taylor
2
谢谢您提供算法和解释,而不仅仅是代码!我希望更多的解决方案都能像这样写。 - delrocco
谢谢,这个很好用。解释也真的帮了很多。 - J-Cake
如果图像框中的x,y可以是任何值而不仅仅是0,0呢? - Dominic

28

根据Eric的建议,我会这样做:

private static Size ExpandToBound(Size image, Size boundingBox)
{       
    double widthScale = 0, heightScale = 0;
    if (image.Width != 0)
        widthScale = (double)boundingBox.Width / (double)image.Width;
    if (image.Height != 0)
        heightScale = (double)boundingBox.Height / (double)image.Height;                

    double scale = Math.Min(widthScale, heightScale);

    Size result = new Size((int)(image.Width * scale), 
                        (int)(image.Height * scale));
    return result;
}

这些强制转换可能有点过度了,但我只是试图在计算中保留精度。


不要忘记 return result; - Brian Chavez

8

如果要进行纵横比填充而不是纵横比适合,则使用较大的比率。也就是说,将Matt的代码从Math.Min更改为Math.Max。

(纵横比填充不会使边界框中留下任何空白,但可能会使一些图像超出边界,而纵横比适合则不会使图像超出边界,但可能会使一些边界框留下空白。)


优秀的信息。从其他答案来看,对我来说并不直观。 - Felipe

7
尝试了Warren先生的代码,但结果不够可靠。
例如,
ExpandToBound(new Size(640,480), new Size(66, 999)).Dump();
// {Width=66, Height=49}

ExpandToBound(new Size(640,480), new Size(999,50)).Dump();
// {Width=66, Height=50}

在另一个地方可以看到,高度分别为49和50。

这是我的代码(基于Warren先生的版本),没有任何差异并进行了轻微的重构:

// Passing null for either maxWidth or maxHeight maintains aspect ratio while
//        the other non-null parameter is guaranteed to be constrained to
//        its maximum value.
//
//  Example: maxHeight = 50, maxWidth = null
//    Constrain the height to a maximum value of 50, respecting the aspect
//    ratio, to any width.
//
//  Example: maxHeight = 100, maxWidth = 90
//    Constrain the height to a maximum of 100 and width to a maximum of 90
//    whichever comes first.
//
private static Size ScaleSize( Size from, int? maxWidth, int? maxHeight )
{
   if ( !maxWidth.HasValue && !maxHeight.HasValue ) throw new ArgumentException( "At least one scale factor (toWidth or toHeight) must not be null." );
   if ( from.Height == 0 || from.Width == 0 ) throw new ArgumentException( "Cannot scale size from zero." );

   double? widthScale = null;
   double? heightScale = null;

   if ( maxWidth.HasValue )
   {
       widthScale = maxWidth.Value / (double)from.Width;
   }
   if ( maxHeight.HasValue )
   {
       heightScale = maxHeight.Value / (double)from.Height;
   }

   double scale = Math.Min( (double)(widthScale ?? heightScale),
                            (double)(heightScale ?? widthScale) );

   return new Size( (int)Math.Floor( from.Width * scale ), (int)Math.Ceiling( from.Height * scale ) );
}

在最后一行中,你应该在两种情况下都使用Math.Round,因为在你的代码中我们得到了错误的结果。 - Thomas

5
以下代码可以产生更准确的结果:
    public static Size CalculateResizeToFit(Size imageSize, Size boxSize)
    {
        // TODO: Check for arguments (for null and <=0)
        var widthScale = boxSize.Width / (double)imageSize.Width;
        var heightScale = boxSize.Height / (double)imageSize.Height;
        var scale = Math.Min(widthScale, heightScale);
        return new Size(
            (int)Math.Round((imageSize.Width * scale)),
            (int)Math.Round((imageSize.Height * scale))
            );
    }

3

这是一段Python代码,也许可以指引您朝正确的方向发展:

def fit_within_box(box_width, box_height, width, height):
    """
    Returns a tuple (new_width, new_height) which has the property
    that it fits within box_width and box_height and has (close to)
    the same aspect ratio as the original size
    """
    new_width, new_height = width, height
    aspect_ratio = float(width) / float(height)

    if new_width > box_width:
        new_width = box_width
        new_height = int(new_width / aspect_ratio)

    if new_height > box_height:
        new_height = box_height
        new_width = int(new_height * aspect_ratio)

    return (new_width, new_height)

1
如果图像需要缩小而不是扩展以适应容器,则您的代码有效。我认为您应该像这样更新您的条件语句: if new_width > box_width or new_height < box_height: 和: if new_height > box_height or new_width < box_width: - Nicolas Le Thierry d'Ennequin

3

非常简单。 :) 问题在于找到需要将宽度和高度相乘的因子。解决方案是尝试使用一个因子,如果不适用,则使用另一个。所以...

private float ScaleFactor(Rectangle outer, Rectangle inner)
{
    float factor = (float)outer.Height / (float)inner.Height;
    if ((float)inner.Width * factor > outer.Width) // Switch!
        factor = (float)outer.Width / (float)inner.Width;
    return factor;
}

要将图片(pctRect)适应于窗口(wndRect),请按照以下方式调用

float factor=ScaleFactor(wndRect, pctRect); // Outer, inner
RectangleF resultRect=new RectangleF(0,0,pctRect.Width*factor,pctRect.Height*Factor)

1

根据之前的答案,这里是一个Javascript函数:

/**
* fitInBox
* Constrains a box (width x height) to fit in a containing box (maxWidth x maxHeight), preserving the aspect ratio
* @param width      width of the box to be resized
* @param height     height of the box to be resized
* @param maxWidth   width of the containing box
* @param maxHeight  height of the containing box
* @param expandable (Bool) if output size is bigger than input size, output is left unchanged (false) or expanded (true)
* @return           {width, height} of the resized box
*/
function fitInBox(width, height, maxWidth, maxHeight, expandable) {
    "use strict";

    var aspect = width / height,
        initWidth = width,
        initHeight = height;

    if (width > maxWidth || height < maxHeight) {
        width = maxWidth;
        height = Math.floor(width / aspect);
    }

    if (height > maxHeight || width < maxWidth) {
        height = maxHeight;
        width = Math.floor(height * aspect);
    }

    if (!!expandable === false && (width >= initWidth || height >= initHeight)) {
        width = initWidth;
        height = initHeight;
    }

    return {
        width: width,
        height: height
    };
}

0

基于Jason的答案编写的Python代码,修复了放大问题,并重新排列参数以实现常规参数传递(img.shape)。

def fit_within_box(box_height, box_width, height, width):
    """
    Returns a tuple (new_width, new_height) which has the property
    that it fits within box_width and box_height and has (close to)
    the same aspect ratio as the original size
    """
    new_width, new_height = width, height
    aspect_ratio = float(width) / float(height)

    if new_width > box_width or new_height < box_height:
        new_width = box_width
        new_height = int(new_width / aspect_ratio)

    if new_height > box_height or new_width < box_width:
        new_height = box_height
        new_width = int(new_height * aspect_ratio)

    return new_height, new_width

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