【问题标题】:How do you calculate the axis-aligned bounding box of an ellipse?你如何计算椭圆的轴对齐边界框?
【发布时间】:2010-09-10 09:44:11
【问题描述】:

如果椭圆的长轴是垂直的或者水平的,边界框的计算很容易,但是椭圆旋转的时候呢?

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

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

编辑:澄清一下,我需要轴对齐的边界框,即它不应该与椭圆一起旋转,但要与 x 轴保持对齐,因此无法转换边界框。

【问题讨论】:

    标签: math graphics geometry


    【解决方案1】:

    这是基于上述答案的打字稿功能。

    export function getRotatedEllipseBounds(
      x: number,
      y: number,
      rx: number,
      ry: number,
      rotation: number
    ) {
      const c = Math.cos(rotation)
      const s = Math.sin(rotation)
      const w = Math.hypot(rx * c, ry * s)
      const h = Math.hypot(rx * s, ry * c)
    
      return {
        minX: x + rx - w,
        minY: y + ry - h,
        maxX: x + rx + w,
        maxY: y + ry + h,
        width: w * 2,
        height: h * 2,
      }
    }
    

    【讨论】:

      【解决方案2】:

      这是一个在 javascript 中围绕旋转椭圆的边界框的简单示例: https://jsfiddle.net/rkn61mjL/1/

      这个想法非常简单,不需要复杂的计算和求解梯度:

      1. 计算一个简单的非旋转椭圆边界框:

        let p1 = [centerX - radiusX, centerY - radiusY];
        let p2 = [centerX + radiusX, centerY - radiusY];
        let p3 = [centerX + radiusX, centerY + radiusY];
        let p4 = [centerX - radiusX, centerY + radiusY];
        
      2. 围绕椭圆中心旋转所有四个点:

        p1 = [(p1[0]-centerX) * Math.cos(radians) - (p1[1]-centerY) *  Math.sin(radians) + centerX,
            (p1[0]-centerX) * Math.sin(radians) + (p1[1]-centerY) * Math.cos(radians) + centerY];        
        p2 = [(p2[0]-centerX) * Math.cos(radians) - (p2[1]-centerY) *  Math.sin(radians) + centerX,
            (p2[0]-centerX) * Math.sin(radians) + (p2[1]-centerY) * Math.cos(radians) + centerY];        
        p3 = [(p3[0]-centerX) * Math.cos(radians) - (p3[1]-centerY) *  Math.sin(radians) + centerX,
            (p3[0]-centerX) * Math.sin(radians) + (p3[1]-centerY) * Math.cos(radians) + centerY];        
        p4 = [(p4[0]-centerX) * Math.cos(radians) - (p4[1]-centerY) *  Math.sin(radians) + centerX,
            (p4[0]-centerX) * Math.sin(radians) + (p4[1]-centerY) * Math.cos(radians) + centerY];
        

      【讨论】:

        【解决方案3】:

        这相对简单但有点难以解释,因为您没有向我们提供您表示椭圆的方式。有很多方法可以做到这一点..

        无论如何,一般原理是这样的:您不能直接计算轴对齐的边界框。但是,您可以将 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。

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

        如果你这样做,方程变成:

          // 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;
        

        【讨论】:

        • 第三行代码中有错字。我第二个= 应该是*。我认为这对每个人来说都是显而易见的。我无法编辑它,因为编辑必须至少有 6 个字符,而这只是一个要编辑的字符。
        【解决方案4】:

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

        // 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;
        }
        

        【讨论】:

          【解决方案5】:

          布赖恩·约翰·尼尔森。 我已将您的代码转录为 c# - ellipseAngle 现在以度为单位:

          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);
          }
          

          【讨论】:

          • 我认为应该是ellipseCenterY - height * 0.5(减号,而不是加号)。否则工作完美。
          【解决方案6】:

          这是我的函数,用于找到具有任意方向的椭圆的紧密拟合矩形

          我有opencv rect和实现点:

          cg - 椭圆的中心

          大小 - 椭圆的长轴、短轴

          角度 - 椭圆的方向

          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;
          }
          

          【讨论】:

            【解决方案7】:

            如果椭圆由其焦点和偏心度给出,这是该情况的公式(对于由轴长度、中心和角度给出的情况,请参见例如 user1789690 的答案) .

            即如果焦点是(x0, y0)和(x1, y1),偏心率是e,那么

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

            在哪里

            dx = x1-x0
            dy = y1-y0
            dx2 = dx*dx
            dy2 = dy*dy
            k2 = 1.0/(e*e)
            

            我从 user1789690 和 Johan Nilsson 的答案中推导出公式。

            【讨论】:

              【解决方案8】:

              此代码基于上面贡献的代码 user1789690,但在 Delphi 中实现。我已经对此进行了测试,据我所知,它运行良好。我花了一整天的时间寻找一个算法或一些代码,测试了一些不起作用的,我很高兴终于找到了上面的代码。我希望有人觉得这很有用。此代码将计算旋转椭圆的边界框。边界框是轴对齐的,不随椭圆旋转。半径是旋转之前的椭圆。

              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;
              

              【讨论】:

                【解决方案9】:

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

                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:

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

                =>

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

                这应该为您提供许多 t 解决方案(您对其中两个感兴趣),将其插入 [1] 以获得您的最大值和最小值 x。

                重复 [2]:

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

                =>

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

                让我们尝试一个例子:

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

                [1] =>

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

                [3] =>

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

                =>

                t = -0.4636 + n*PI
                

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

                所以我们得到:

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

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

                【讨论】:

                • 谢谢。这行得通,除非你在等式二中有错字。减号应该是加号。
                • 我对这个答案不太清楚:为什么我们只对 t = -0.4636 和 t = -3.6052 感兴趣?我看到这些结果来自用 0 和 -1 代替方程 t = -0.4636 + n*PI 中的变量“n”,但不知道这些特定值的重要性。是不是因为其他 t 值在插入我们的原始方程时会产生重复的笛卡尔坐标?
                • @Caleb:完全正确。只有两个独特的解决方案适合单个完整的 2*PI 旋转(因为我们有 ...+n*PI 项)。通过选择 t 的任意 2 个连续值,我们确保将 t 的这些值插入等式中得到两个唯一答案。您可以通过尝试t 的其他值来确认这一点。
                • 显然,您的解决方案不适用于 phi=90° 和 phi=270°
                【解决方案10】:

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

                我大致是这样实现的:

                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);
                
                【解决方案11】:

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

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

                据此,您应该能够推导出哪个 t dx/dt 或 dy/dt 变为 0。

                【讨论】:

                • 我现在感觉很慢,花了我很长时间才写出我的答案T.T
                猜你喜欢
                • 1970-01-01
                • 2022-11-12
                • 1970-01-01
                • 2011-05-10
                • 1970-01-01
                • 1970-01-01
                • 2019-12-24
                • 2010-12-18
                • 1970-01-01
                相关资源
                最近更新 更多