从旋转矩形中计算出边界框坐标

82

我有一个矩形左上角点的坐标,以及它的宽度、高度和旋转角度从0到180度和从-0到-180度。

我想要获取实际包围该矩形的框的边界坐标。

有什么简单方法来计算包围框的坐标呢?

  • 最小y值、最大y值、最小x值、最大x值吗?

点A不一定在最小y边界上,它可以在任何位置。

如果需要,我可以使用AS3中的矩阵变换工具包。


1
图片无法显示..(图片上写着:点击发现Imageshack)!!! - Pratham
对不起,我的错。不知道能否从谷歌存档或其他地方找回它。 - coulix
1
什么是A点? - Xhark
12个回答

91
  • 将四个角的坐标转换
  • 找到所有x值中最小的一个,赋值给min_x
  • 找到所有x值中最大的一个,赋值给max_x
  • y值同理
  • 边界框为(min_x,min_y), (min_x,max_y), (max_x,max_y), (max_x,min_y)

据我所知,并没有任何皇家大道可以让你更快地到达那里。

如果你想知道如何转换坐标,请尝试:

x2 = x0+(x-x0)*cos(theta)+(y-y0)*sin(theta)
y2 = y0-(x-x0)*sin(theta)+(y-y0)*cos(theta)

其中(x0,y0)是你要旋转的中心点。根据三角函数(它们是否需要使用度数或弧度)以及您指定角度的坐标系的方向/符号,您可能需要调整此参数。


3
由于对称性,实际上只需要转换两个角落,如果再多思考一下,只需旋转1个角落即可。 - ysap
1
@ysap 这仅在您围绕矩形中心旋转的情况下才有效。 - sidon
@sidon - 这是真的。然而,这是大多数绘图程序的做法。 - ysap
@sidon - 尽管如此,我认为即使旋转点是角落的情况下,你也只需要进行2次三角函数计算,而不是4次。 - ysap
6
@MarkusQ提到的x2的方程应该是x0+(x-x0)*cos(theta)-(y-y0)*sin(theta),而不是x0+(x-x0)*cos(theta)+(y-y0)*sin(theta),我说得对吗? - Mihir
显示剩余2条评论

29

我知道你在询问ActionScript,但是以防万一有人在这里寻找iOS或OS-X的答案,它是这样的:

+ (CGRect) boundingRectAfterRotatingRect: (CGRect) rect toAngle: (float) radians
{
    CGAffineTransform xfrm = CGAffineTransformMakeRotation(radians);
    CGRect result = CGRectApplyAffineTransform (rect, xfrm);

    return result;
}

如果你的操作系统可以为你完成所有的艰难工作,就让它来做吧! :)

Swift:

如果您的操作系统提供了自动化的解决方案,请务必使用! :)
func boundingRectAfterRotatingRect(rect: CGRect, toAngle radians: CGFloat) -> CGRect {
    let xfrm = CGAffineTransformMakeRotation(radians)
    return CGRectApplyAffineTransform (rect, xfrm)
}

2
想要补充一下,角度必须是弧度制而不是角度制。这可能会为您节省一些时间。 ;) - Git.Coach

13

MarkusQ提出的方法完美地工作了,但请记住,如果您已经有点A,则不需要转换其他三个角。

一种更有效的替代方法是测试旋转角度所在的象限,然后直接计算答案。这更有效,因为您最多只需要两个 if 语句(检查角度),而另一种方法最坏情况下需要十二个 if 语句(当检查其他三个角以查看它们是否大于当前最大值或小于当前最小值时,每个分量需要六个)。我认为。

下面显示了基本算法,它仅使用了一系列应用勾股定理的方法。我将旋转角度标记为theta,并将其在伪代码中表示为度数。

ct = cos( theta );
st = sin( theta );

hct = h * ct;
wct = w * ct;
hst = h * st;
wst = w * st;

if ( theta > 0 )
{
    if ( theta < 90 degrees )
    {
        // 0 < theta < 90
        y_min = A_y;
        y_max = A_y + hct + wst;
        x_min = A_x - hst;
        x_max = A_x + wct;
    }
    else
    {
        // 90 <= theta <= 180
        y_min = A_y + hct;
        y_max = A_y + wst;
        x_min = A_x - hst + wct;
        x_max = A_x;
    }
}
else
{
    if ( theta > -90 )
    {
        // -90 < theta <= 0
        y_min = A_y + wst;
        y_max = A_y + hct;
        x_min = A_x;
        x_max = A_x + wct - hst;
    }
    else
    {
        // -180 <= theta <= -90
        y_min = A_y + wst + hct;
        y_max = A_y;
        x_min = A_x + wct;
        x_max = A_x - hst;
    }
}

这种方法假定你有你所说的东西,即点A和在范围[-180, 180]内的theta值。我还假设theta沿顺时针方向增加,因为在你的图表中被旋转30度的矩形似乎表明你正在使用这种方式,我不确定右侧的部分试图表示什么。如果方向相反,只需交换对称子句和st项的符号。


我知道这已经很老了,但它是谷歌的首个搜索结果,所以在这里我会记录一下:在此之前,只需将比例应用于h和w,它就应该可以正常工作。这个答案目前是我最喜欢的解决方案,因为它将计算分解成了小块。在iOS中有2个分支和一些NEON魔法,4-6个操作或更少,具体取决于你的技巧。 - Diniden
人们一直错误地编辑第二个if语句中的“度数”。这是伪代码,因此它永远不会编译。明确说明“度数”的目的是因为theta显然不是以度数表示的。当实现正确的工作代码以获取角度单位时,它作为提醒存在。 - Troubadour
好的,为了避免混淆,如果你把所有的if语句改成if (theta > 0度)if (theta > -90度),那就更加一致了。不一致会促使人们对其进行编辑。 - wcmatthysen

7
    fitRect: function( rw,rh,radians ){
            var x1 = -rw/2,
                x2 = rw/2,
                x3 = rw/2,
                x4 = -rw/2,
                y1 = rh/2,
                y2 = rh/2,
                y3 = -rh/2,
                y4 = -rh/2;

            var x11 = x1 * Math.cos(radians) + y1 * Math.sin(radians),
                y11 = -x1 * Math.sin(radians) + y1 * Math.cos(radians),
                x21 = x2 * Math.cos(radians) + y2 * Math.sin(radians),
                y21 = -x2 * Math.sin(radians) + y2 * Math.cos(radians), 
                x31 = x3 * Math.cos(radians) + y3 * Math.sin(radians),
                y31 = -x3 * Math.sin(radians) + y3 * Math.cos(radians),
                x41 = x4 * Math.cos(radians) + y4 * Math.sin(radians),
                y41 = -x4 * Math.sin(radians) + y4 * Math.cos(radians);

            var x_min = Math.min(x11,x21,x31,x41),
                x_max = Math.max(x11,x21,x31,x41);

            var y_min = Math.min(y11,y21,y31,y41);
                y_max = Math.max(y11,y21,y31,y41);

            return [x_max-x_min,y_max-y_min];
        }

3

如果您正在使用GDI+,可以创建一个新的GraphicsPath ->添加任何点或形状 ->应用旋转变换 ->使用GraphicsPath.GetBounds(),它将返回一个包围您旋转的形状的矩形。

(编辑) VB.Net示例

Public Shared Sub RotateImage(ByRef img As Bitmap, degrees As Integer)
' https://dev59.com/8nRB5IYBdhLWcg3wcm2d
'
Using gp As New GraphicsPath
  gp.AddRectangle(New Rectangle(0, 0, img.Width, img.Height))

  Dim translateMatrix As New Matrix
  translateMatrix.RotateAt(degrees, New PointF(img.Width \ 2, img.Height \ 2))
  gp.Transform(translateMatrix)

  Dim gpb = gp.GetBounds

  Dim newwidth = CInt(gpb.Width)
  Dim newheight = CInt(gpb.Height)

  ' http://www.codeproject.com/Articles/58815/C-Image-PictureBox-Rotations
  '
  Dim rotatedBmp As New Bitmap(newwidth, newheight)

  rotatedBmp.SetResolution(img.HorizontalResolution, img.VerticalResolution)

  Using g As Graphics = Graphics.FromImage(rotatedBmp)
    g.Clear(Color.White)
    translateMatrix = New Matrix
    translateMatrix.Translate(newwidth \ 2, newheight \ 2)
    translateMatrix.Rotate(degrees)
    translateMatrix.Translate(-img.Width \ 2, -img.Height \ 2)
    g.Transform = translateMatrix
    g.DrawImage(img, New PointF(0, 0))
  End Using
  img.Dispose()
  img = rotatedBmp
End Using

结束子程序


2
/**
     * Applies the given transformation matrix to the rectangle and returns
     * a new bounding box to the transformed rectangle.
     */
    public static function getBoundsAfterTransformation(bounds:Rectangle, m:Matrix):Rectangle {
        if (m == null) return bounds;

        var topLeft:Point = m.transformPoint(bounds.topLeft);
        var topRight:Point = m.transformPoint(new Point(bounds.right, bounds.top));
        var bottomRight:Point = m.transformPoint(bounds.bottomRight);
        var bottomLeft:Point = m.transformPoint(new Point(bounds.left, bounds.bottom));

        var left:Number = Math.min(topLeft.x, topRight.x, bottomRight.x, bottomLeft.x);
        var top:Number = Math.min(topLeft.y, topRight.y, bottomRight.y, bottomLeft.y);
        var right:Number = Math.max(topLeft.x, topRight.x, bottomRight.x, bottomLeft.x);
        var bottom:Number = Math.max(topLeft.y, topRight.y, bottomRight.y, bottomLeft.y);
        return new Rectangle(left, top, right - left, bottom - top);
    }

2
在这个问题中,用户说他有以度为单位的旋转。您的解决方案需要一个变换矩阵。如何从以度为单位的旋转转换为旋转变换矩阵? - pgreen2

2
尽管Code Guru提到了GetBounds()方法,但我注意到这个问题被标记为as3、flex,所以这里有一个as3片段来说明这个想法。
var box:Shape = new Shape();
box.graphics.beginFill(0,.5);
box.graphics.drawRect(0,0,100,50);
box.graphics.endFill();
box.rotation = 20;
box.x = box.y = 100;
addChild(box);

var bounds:Rectangle = box.getBounds(this);

var boundingBox:Shape = new Shape();
boundingBox.graphics.lineStyle(1);
boundingBox.graphics.drawRect(bounds.x,bounds.y,bounds.width,bounds.height);
addChild(boundingBox);

我注意到有两种方法似乎可以做同样的事情:getBounds()和getRect()


getRect执行与getBounds相同的操作,但不包括对象上的描边:http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/display/DisplayObject.html#getRect%28%29 - Danny Parker

1

当矩形围绕其中心旋转时,计算是微不足道的:

function getBoundingBox(rX, rY, rW, rH, rA) {
  const absCosRA = Math.abs(Math.cos(rA));
  const absSinRA = Math.abs(Math.sin(rA));

  const bbW = rW * absCosRA + rH * absSinRA;
  const bbH = rW * absSinRA + rH * absCosRA;
  
  const bbX = rX - (bbW - rW) / 2;
  const bbY = rY - (bbH - rH) / 2;

  return { x: bbX, y: bbY, w: bbW, h: bbH };
}

谢谢你为我解决了这些推导问题。这是一个优雅的解决方案,应该被接受作为答案。我认为除了游戏开发领域的程序员外,很少有人熟悉变换和旋转矩阵,所以我不会把它看作是微不足道的。 - Florian Enner

1
应用旋转矩阵到你的角点上,然后使用所得到的x、y坐标的最小/最大值来定义你的新边界框。

1

以下是我开源库中的三个函数。这些函数在Java中进行了全面测试,但公式可以轻松转换为任何语言。

函数签名如下:

public static float getAngleFromPoint(final Point centerPoint, final Point touchPoint)

public static float getTwoFingerDistance(float firstTouchX, float firstTouchY, float secondTouchX, float secondTouchY)

Point getPointFromAngle(final double angle, final double radius)

该解决方案假设像素密度均匀分布。 在旋转对象之前,请执行以下操作:

  1. 使用getAngleFromPoint函数计算从中心点到右上角的角度(假设返回20度),意味着左上角是-20度或340度。

  2. 使用getTwoFingerDistance函数返回中心点和右上角之间的对角线距离(这个距离显然应该对所有角落都相同,这个距离将在下一步计算中使用)。

  3. 现在假设我们将对象顺时针旋转30度。我们现在知道右上角必须在50度处,左上角在10度处。

  4. 现在您应该能够在左上角和右上角上使用getPointFromAngle函数,使用步骤2返回的半径。 从右上角乘以2的X位置应该给出新宽度,从左上角乘以2的Y位置应该给出新高度。

以上4个步骤应根据您旋转对象的程度放入条件中,否则您可能会返回高度作为宽度,宽度作为高度。

请注意,角度函数以0-1的因数表示,而不是0-360(只需在适当的地方乘以或除以360即可):

//从两个点中获取一个角度,以0-1的因数表示(0表示0/360,0.25表示90度等)

public float getAngleFromPoint(final Point centerPoint, final Point touchPoint) {

    float returnVal = 0;

    //+0 - 0.5
    if(touchPoint.x > centerPoint.x) {

        returnVal = (float) (Math.atan2((touchPoint.x - centerPoint.x), (centerPoint.y - touchPoint.y)) * 0.5 / Math.PI);

    }
    //+0.5
    else if(touchPoint.x < centerPoint.x) {

        returnVal = (float) (1 - (Math.atan2((centerPoint.x - touchPoint.x), (centerPoint.y - touchPoint.y)) * 0.5 / Math.PI));

    }//End if(touchPoint.x > centerPoint.x)

    return returnVal;

}

//测量两点之间的对角线距离

public float getTwoFingerDistance(final float firstTouchX, final float firstTouchY, final float secondTouchX, final float secondTouchY) {

    float pinchDistanceX = 0;
    float pinchDistanceY = 0;

    if(firstTouchX > secondTouchX) {

        pinchDistanceX = Math.abs(secondTouchX - firstTouchX);

    }
    else if(firstTouchX < secondTouchX) {

        pinchDistanceX = Math.abs(firstTouchX - secondTouchX);

    }//End if(firstTouchX > secondTouchX)

    if(firstTouchY > secondTouchY) {

        pinchDistanceY = Math.abs(secondTouchY - firstTouchY);

    }
    else if(firstTouchY < secondTouchY) {

        pinchDistanceY = Math.abs(firstTouchY - secondTouchY);

    }//End if(firstTouchY > secondTouchY)

    if(pinchDistanceX == 0 && pinchDistanceY == 0) {

        return 0;

    }
    else {

        pinchDistanceX = (pinchDistanceX * pinchDistanceX);
        pinchDistanceY = (pinchDistanceY * pinchDistanceY);
        return (float) Math.abs(Math.sqrt(pinchDistanceX + pinchDistanceY));

    }//End if(pinchDistanceX == 0 && pinchDistanceY == 0)

}

//在给定半径的情况下,从角度获取XY坐标(角度以0-1因子表示,其中0为0/360度,0.75为270度等)
public Point getPointFromAngle(final double angle, final double radius) {

    final Point coords = new Point();
    coords.x = (int) (radius * Math.sin((angle) * 2 * Math.PI));
    coords.y = (int) -(radius * Math.cos((angle) * 2 * Math.PI));

    return coords;

}

这些代码片段来自我的开源库:https://bitbucket.org/warwick/hgdialrepohttps://bitbucket.org/warwick/hacergestov2,一个是用于 Android 的手势库,另一个是用于 Android 的拨号控件。此外,还有一个拨号控件的 OpenGLES 2.0 实现:https://bitbucket.org/warwick/hggldial。请保留 HTML 标签。

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