首页 / 知识

关于数学:如何计算椭圆的轴对齐边界框?

2023-04-16 20:37:00

How do you calculate the axis-aligned bounding box of an ellipse?

如果椭圆的主轴是垂直或水平,则很容易计算边界框,但是旋转椭圆时会怎样呢?

到目前为止,我唯一想到的方法是计算周长周围的所有点,并找到最大/最小x和y值。 似乎应该有一个更简单的方法。

如果有一个函数(在数学意义上)以任意角度描述椭圆,那么我可以使用其导数来找到斜率为零或未定义的点,但我似乎找不到一个。

编辑:为澄清起见,我需要与轴对齐的边界框,即它不应与椭圆一起旋转,而应与x轴保持对齐,因此变换边界框将不起作用。


您可以对以任意角度旋转的椭圆尝试使用参数化方程:

1
2
x = h + a*cos(t)*cos(phi) - b*sin(t)*sin(phi)  [1]
y = k + b*sin(t)*cos(phi) + a*cos(t)*sin(phi)  [2]

...椭圆具有中心(h,k)半长轴a和半短轴b,并旋转角度phi。

然后可以区分并求解梯度= 0:

1
0 = dx/dt = -a*sin(t)*cos(phi) - b*cos(t)*sin(phi)

=>

1
tan(t) = -b*tan(phi)/a   [3]

哪个应该为您提供许多t的解决方案(您感兴趣的两个),然后将其重新插入[1]以获取最大和最小x。

重复[2]:

1
0 = dy/dt = b*cos(t)*cos(phi) - a*sin(t)*sin(phi)

=>

1
tan(t) = b*cot(phi)/a  [4]

让我们尝试一个例子:

考虑一个椭圆(0,0)的a = 2,b = 1,由PI / 4旋转:

[1] =>

1
x = 2*cos(t)*cos(PI/4) - sin(t)*sin(PI/4)

[3] =>

1
tan(t) = -tan(PI/4)/2 = -1/2

=>

1
t = -0.4636 + n*PI

我们对t = -0.4636和t = -3.6052有兴趣

这样我们得到:

1
x = 2*cos(-0.4636)*cos(PI/4) - sin(-0.4636)*sin(PI/4) = 1.5811

1
x = 2*cos(-3.6052)*cos(PI/4) - sin(-3.6052)*sin(PI/4) = -1.5811

我在http://www.iquilezles.org/www/articles/ellipses/ellipses.htm中找到了一个简单的公式(忽略了z轴)。

我大致是这样实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
num ux = ellipse.r1 * cos(ellipse.phi);
num uy = ellipse.r1 * sin(ellipse.phi);
num vx = ellipse.r2 * cos(ellipse.phi+PI/2);
num vy = ellipse.r2 * sin(ellipse.phi+PI/2);

num bbox_halfwidth = sqrt(ux*ux + vx*vx);
num bbox_halfheight = sqrt(uy*uy + vy*vy);

Point bbox_ul_corner = new Point(ellipse.center.x - bbox_halfwidth,
                                 ellipse.center.y - bbox_halfheight);

Point bbox_br_corner = new Point(ellipse.center.x + bbox_halfwidth,
                                 ellipse.center.y + bbox_halfheight);

这是相对简单的,但由于您没有给我们提供椭圆的表示方式,因此很难解释。有很多方法可以做到。

无论如何,一般原则是这样的:您不能直接计算轴对齐的边界框。但是,您可以将x和y中的椭圆的极值计算为2D空间中的点。

为此,将等式x(t)= ellipse_equation(t)和y(t)= ellipse_equation(t)就足够了。得到它的一阶导数并为它的根求解。由于我们正在处理基于三角的椭圆,这很简单。您应该以一个方程式结束,该方程式可以通过atan,acos或asin求根。

提示:要检查您的代码,请使用不旋转的椭圆进行尝试:您的根应为0,Pi / 2,Pi和3 * Pi / 2。

对每个轴(x和y)执行此操作。您将获得最多四个根(如果您的椭圆退化了,则更少,例如半径之一为零)。求出根部的位置,即可得到椭圆的所有极限点。

现在您快到了。获取椭圆的边界框就像扫描这四个点的xmin,xmax,ymin和ymax一样简单。

顺便说一句-如果您在查找椭圆方程时遇到问题:请尝试将其简化为具有中心,两个半径和围绕中心的旋转角度的椭圆对齐的轴。

如果这样做,则等式变为:

1
2
3
4
5
6
7
  // the ellipse unrotated:
  temp_x(t) = radius.x * cos(t);
  temp_y(t) = radius.y * sin(t);

  // the ellipse with rotation applied:
  x(t) = temp_x(t) * cos(angle) - temp_y(t) * sin(angle) + center.x;
  y(t) = temp_x(t) * sin(angle) + temp_y(t) * cos(angle) + center.y;

Brilian Johan Nilsson。
我已经将您的代码转录为C#-ellipseAngle现在是度数:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static RectangleF EllipseBoundingBox(int ellipseCenterX, int ellipseCenterY, int ellipseRadiusX, int ellipseRadiusY, double ellipseAngle)
{
    double angle = ellipseAngle * Math.PI / 180;
    double a = ellipseRadiusX * Math.Cos(angle);
    double b = ellipseRadiusY * Math.Sin(angle);
    double c = ellipseRadiusX * Math.Sin(angle);
    double d = ellipseRadiusY * Math.Cos(angle);
    double width = Math.Sqrt(Math.Pow(a, 2) + Math.Pow(b, 2)) * 2;
    double height = Math.Sqrt(Math.Pow(c, 2) + Math.Pow(d, 2)) * 2;
    var x= ellipseCenterX - width * 0.5;
    var y= ellipseCenterY + height * 0.5;
    return new Rectangle((int)x, (int)y, (int)width, (int)height);
}

我认为最有用的公式就是这个。从原点以phi角度旋转的省略号具有以下公式:

alt text

alt text

其中(h,k)是中心,a和b的长轴和短轴的大小以及t在-pi到pi之间变化。

由此,您应该能够得出t dx / dt或dy / dt变为0的原因。


如果使用OpenCV / C ++并使用cv::fitEllipse(..)函数,则可能需要椭圆的边界矩形。 在这里,我使用麦克的答案提出了一个解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// tau = 2 * pi, see tau manifest
const double TAU = 2 * std::acos(-1);

cv::Rect calcEllipseBoundingBox(const cv::RotatedRect &anEllipse)
{
    if (std::fmod(std::abs(anEllipse.angle), 90.0) <= 0.01) {
        return anEllipse.boundingRect();
    }

    double phi   = anEllipse.angle * TAU / 360;
    double major = anEllipse.size.width  / 2.0;
    double minor = anEllipse.size.height / 2.0;

    if (minor > major) {
        std::swap(minor, major);
        phi += TAU / 4;
    }

    double cosPhi = std::cos(phi), sinPhi = std::sin(phi);
    double tanPhi = sinPhi / cosPhi;

    double tx = std::atan(-minor * tanPhi / major);
    cv::Vec2d eqx{ major * cosPhi, - minor * sinPhi };
    double x1 = eqx.dot({ std::cos(tx),           std::sin(tx)           });
    double x2 = eqx.dot({ std::cos(tx + TAU / 2), std::sin(tx + TAU / 2) });

    double ty = std::atan(minor / (major * tanPhi));
    cv::Vec2d eqy{ major * sinPhi, minor * cosPhi };
    double y1 = eqy.dot({ std::cos(ty),           std::sin(ty)           });
    double y2 = eqy.dot({ std::cos(ty + TAU / 2), std::sin(ty + TAU / 2) });

    cv::Rect_<float> bb{
        cv::Point2f(std::min(x1, x2), std::min(y1, y2)),
        cv::Point2f(std::max(x1, x2), std::max(y1, y2))
    };

    return bb + anEllipse.center;
}


这里是椭圆由其焦点和偏心率给出的情况的公式(对于由轴长,中心和角度给出的情况,参见例如user1789690的回答)。

即,如果焦点是(x0,y0)和(x1,y1)并且偏心距是e,则

1
2
bbox_halfwidth  = sqrt(k2*dx2 + (k2-1)*dy2)/2
bbox_halfheight = sqrt((k2-1)*dx2 + k2*dy2)/2

哪里

1
2
3
4
5
dx = x1-x0
dy = y1-y0
dx2 = dx*dx
dy2 = dy*dy
k2 = 1.0/(e*e)

我从user1789690和Johan Nilsson的答案中得出了公式。


这是我的函数,用于查找紧配合矩形以任意方向椭圆

我有opencv rect和实施点:

cg-椭圆的中心

大小-椭圆的长,短轴

椭圆的角度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
cv::Rect ellipse_bounding_box(const cv::Point2f &cg, const cv::Size2f &size, const float angle) {

    float a = size.width / 2;
    float b = size.height / 2;
    cv::Point pts[4];

    float phi = angle * (CV_PI / 180);
    float tan_angle = tan(phi);
    float t = atan((-b*tan_angle) / a);
    float x = cg.x + a*cos(t)*cos(phi) - b*sin(t)*sin(phi);
    float y = cg.y + b*sin(t)*cos(phi) + a*cos(t)*sin(phi);
    pts[0] = cv::Point(cvRound(x), cvRound(y));

    t = atan((b*(1 / tan(phi))) / a);
    x = cg.x + a*cos(t)*cos(phi) - b*sin(t)*sin(phi);
    y = cg.y + b*sin(t)*cos(phi) + a*cos(t)*sin(phi);
    pts[1] = cv::Point(cvRound(x), cvRound(y));

    phi += CV_PI;
    tan_angle = tan(phi);
    t = atan((-b*tan_angle) / a);
    x = cg.x + a*cos(t)*cos(phi) - b*sin(t)*sin(phi);
    y = cg.y + b*sin(t)*cos(phi) + a*cos(t)*sin(phi);
    pts[2] = cv::Point(cvRound(x), cvRound(y));

    t = atan((b*(1 / tan(phi))) / a);
    x = cg.x + a*cos(t)*cos(phi) - b*sin(t)*sin(phi);
    y = cg.y + b*sin(t)*cos(phi) + a*cos(t)*sin(phi);
    pts[3] = cv::Point(cvRound(x), cvRound(y));

    long left = 0xfffffff, top = 0xfffffff, right = 0, bottom = 0;
    for (int i = 0; i < 4; i++) {
        left = left < pts[i].x ? left : pts[i].x;
        top = top < pts[i].y ? top : pts[i].y;
        right = right > pts[i].x ? right : pts[i].x;
        bottom = bottom > pts[i].y ? bottom : pts[i].y;
    }
    cv::Rect fit_rect(left, top, (right - left) + 1, (bottom - top) + 1);
    return fit_rect;
}

该代码基于上面提供的代码user1789690,但在Delphi中实现。我已经对此进行了测试,据我所知它可以完美运行。我花了一整天的时间寻找算法或某些代码,测试了一些无效的代码,很高兴最终找到上面的代码。我希望有人觉得这有用。此代码将计算旋转椭圆的边界框。边界框与轴对齐,并且不随椭圆旋转。半径是椭圆旋转之前的半径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type

  TSingleRect = record
    X:      Single;
    Y:      Single;
    Width:  Single;
    Height: Single;
  end;

function GetBoundingBoxForRotatedEllipse(EllipseCenterX, EllipseCenterY, EllipseRadiusX,  EllipseRadiusY, EllipseAngle: Single): TSingleRect;
var
  a: Single;
  b: Single;
  c: Single;
  d: Single;
begin
  a := EllipseRadiusX * Cos(EllipseAngle);
  b := EllipseRadiusY * Sin(EllipseAngle);
  c := EllipseRadiusX * Sin(EllipseAngle);
  d := EllipseRadiusY * Cos(EllipseAngle);
  Result.Width  := Hypot(a, b) * 2;
  Result.Height := Hypot(c, d) * 2;
  Result.X      := EllipseCenterX - Result.Width * 0.5;
  Result.Y      := EllipseCenterY - Result.Height * 0.5;
end;


计算边界对齐方法

最新内容

相关内容

猜你喜欢