【问题标题】:Compute the area of intersection between a circle and a triangle?计算圆和三角形的交点面积?
【发布时间】:2010-10-07 02:27:59
【问题描述】:

如何计算三角形(指定为三个 (X,Y) 对)和圆 (X,Y,R) 之间的相交面积?我做了一些搜索无济于事。这是为了工作,不是为了学校。 :)

在 C# 中看起来像这样:

struct { PointF vert[3]; } Triangle;
struct { PointF center; float radius; } Circle;

// returns the area of intersection, e.g.:
// if the circle contains the triangle, return area of triangle
// if the triangle contains the circle, return area of circle
// if partial intersection, figure that out
// if no intersection, return 0
double AreaOfIntersection(Triangle t, Circle c)
{
 ...
}

【问题讨论】:

    标签: algorithm geometry computational-geometry intersection area


    【解决方案1】:

    首先我要提醒我们如何求多边形的面积。完成此操作后,查找多边形和圆的交点的算法应该很容易理解。

    如何求多边形的面积

    让我们看一下三角形的情况,因为所有基本逻辑都出现在那里。假设我们有一个顶点为 (x1,y1)、(x2,y2) 和 (x3,y3) 的三角形,当您逆时针绕三角形时,如下图所示:

    然后你可以通过公式计算面积

    A=(x1 y2 + x2 y3 + x3 y1 - x2y1- x3 y2 - x1y3)/2。

    要了解为什么这个公式有效,让我们重新排列它,使其成为形式

    A=(x1 y2 - x2 y1)/2 + (x2 y3 - x3 y2)/2 + (x3 y1 - x1y3 )/2。

    现在第一项是以下领域,在我们的例子中是积极的:

    如果不清楚绿色区域的面积确实是 (x1 y2 - x2 y1)/2,请阅读this

    第二项是这个领域,又是正面的:

    而第三个区域如下图所示。这次面积是负数

    将这三个加起来我们得到下面的图片

    我们看到三角形外面的绿色区域被红色区域抵消了,所以净面积就是三角形的面积,这说明了为什么我们的公式在这种情况下是正确的。

    我上面所说的是对面积公式为何正确的直观解释。更严格的解释是观察当我们从边缘计算面积时,我们得到的面积与我们从积分 r^2dθ/2 得到的面积相同,因此我们有效地在边界周围积分 r^2dθ/2根据斯托克斯定理,这给出了与在多边形边界区域上积分 rdrdθ 相同的结果。由于在由多边形包围的区域上积分 rdrdθ 给出了面积,我们得出结论,我们的程序必须正确给出面积。

    圆与多边形相交的面积

    现在我们来讨论如何求半径为 R 的圆与多边形的交点面积,如下图所示:

    我们有兴趣找到绿色区域的面积。就像在单个多边形的情况下一样,我们可以将计算分解为为多边形的每一边找到一个区域,然后将这些区域相加。

    我们的第一个区域将如下所示:

    第二个区域看起来像

    第三个区域是

    同样,在我们的案例中,前两个方面是积极的,而第三个方面则是消极的。希望取消能够解决,以便净面积确实是我们感兴趣的区域。让我们看看。

    确实,这些区域的总和就是我们感兴趣的区域。

    同样,我们可以更严格地解释为什么会这样。令 I 为交点定义的区域,令 P 为多边形。然后从前面的讨论中,我们知道我们想要计算 r^2dθ/2 在 I 边界周围的积分。但是,这很难做到,因为它需要找到交点。

    相反,我们对多边形进行了积分。我们在多边形的边界上集成了 max(r,R)^2 dθ/2。为了看看为什么这给出了正确的答案,让我们定义一个函数 π,它将极坐标 (r,θ) 中的一个点带到点 (max(r,R),θ)。引用 π(r)=max(r,R) 和 π(θ)=θ 的坐标函数应该不会造成混淆。然后我们所做的就是在多边形的边界上积分 π(r)^2 dθ/2。

    另一方面,由于 π(θ)=θ,这与在多边形边界上积分 π(r)^2 dπ(θ)/2 相同。

    现在做一个变量的改变,我们发现如果我们在 π(P) 的边界上积分 r^2 dθ/2 会得到相同的答案,其中 π(P) 是 P 在 π 下的图像。

    再次使用斯托克斯定理,我们知道在 π(P) 的边界上积分 r^2 dθ/2 可以得到 π(P) 的面积。换句话说,它给出了与在 π(P) 上积分 dxdy 相同的答案。

    再次使用变量的变化,我们知道 dxdy 在 π(P) 上积分与 Jdxdy 在 P 上积分相同,其中 J 是 π 的雅可比。

    现在我们可以将Jdxdy的积分分成两个区域:圆内部分和圆外部分。现在 π 只在圆中留下点,所以在那里 J=1,所以 P 的这一部分的贡献是 P 位于圆中的部分的面积,即交点的面积。第二个区域是圆外的区域。 J=0,因为 π 将这部分折叠到圆的边界。

    因此我们计算的确实是交叉点的面积。

    现在我们相对确定我们在概念上知道如何找到该区域,让我们更具体地讨论如何计算单个细分的贡献。让我们从我称之为“标准几何”的部分开始。如下所示。

    在标准几何中,边缘从左到右水平移动。它由三个数字描述:xi,边缘开始的 x 坐标,xf,边缘结束的 x 坐标,以及 y,边缘的 y 坐标。

    现在我们看到如果 |y|

    区域 2 的面积就是三角形的面积。但是,我们必须小心标志。我们希望显示的区域为正,因此我们会说该区域为 -(xint - (-xint))y/2。

    要记住的另一件事是,通常,xi 不必小于 -xint,xf 不必大于 xint。

    要考虑的另一种情况是|y| > R. 这种情况比较简单,因为只有一块与图中区域1相似。

    现在我们知道如何从标准几何中的边计算面积,剩下要做的就是描述如何将任何边转换为标准几何。

    但这只是坐标的简单变化。给定一些初始顶点 vi 和最终顶点 vf,新的 x 单位向量将是从 vi 指向 vf 的单位向量。那么 xi 就是 vi 从点入 x 的圆心的位移,而 xf 就是 xi 加上 vi 和 vf 之间的距离。同时y是x与vi到圆心的位移的楔积。

    代码

    算法的描述就完成了,现在是时候写一些代码了。我会用java。

    首先,既然我们使用的是圆,我们应该有一个圆类

    public class Circle {
    
        final Point2D center;
        final double radius;
    
        public Circle(double x, double y, double radius) {
            center = new Point2D.Double(x, y);
            this.radius = radius;
        }
    
        public Circle(Point2D.Double center, double radius) {
            this(center.getX(), center.getY(), radius);
        }
    
        public Point2D getCenter() {
            return new Point2D.Double(getCenterX(), getCenterY());
        }
    
        public double getCenterX() {
            return center.getX();
        }
    
        public double getCenterY() {
            return center.getY();
        }
    
        public double getRadius() {
            return radius;
        }
    
    }
    

    对于多边形,我将使用 java 的 Shape 类。 Shapes 有一个 PathIterator,我可以用它来遍历多边形的边缘。

    现在开始实际工作。一旦完成,我会将遍历边缘、将边缘置于标准几何图形等中的逻辑与计算区域的逻辑分开。这样做的原因是,您将来可能想要计算除该区域之外的其他东西,并且您希望能够重用必须处理迭代边缘的代码。

    所以我有一个通用类,它计算类T 关于我们的多边形圆交点的一些属性。

    public abstract class CircleShapeIntersectionFinder<T> {

    它有三个静态方法来帮助计算几何:

    private static double[] displacment2D(final double[] initialPoint, final double[] finalPoint) {
        return new double[]{finalPoint[0] - initialPoint[0], finalPoint[1] - initialPoint[1]};
    }
    
    private static double wedgeProduct2D(final double[] firstFactor, final double[] secondFactor) {
        return firstFactor[0] * secondFactor[1] - firstFactor[1] * secondFactor[0];
    }
    
    static private double dotProduct2D(final double[] firstFactor, final double[] secondFactor) {
        return firstFactor[0] * secondFactor[0] + firstFactor[1] * secondFactor[1];
    }
    

    有两个实例字段,Circle 只保留圆的副本,currentSquareRadius 保留正方形半径的副本。这可能看起来很奇怪,但我使用的类实际上可以找到整个圆形多边形交叉点的区域。这就是为什么我将其中一个圈子称为“当前”。

    private Circle currentCircle;
    private double currentSquareRadius;
    

    接下来是计算我们要计算的方法:

    public final T computeValue(Circle circle, Shape shape) {
        initialize();
        processCircleShape(circle, shape);
        return getValue();
    }
    

    initialize()getValue() 是抽象的。 initialize() 会将保持总面积的变量设置为零,getValue() 只会返回该面积。 processCircleShape 的定义是

    private void processCircleShape(Circle circle, final Shape cellBoundaryPolygon) {
        initializeForNewCirclePrivate(circle);
        if (cellBoundaryPolygon == null) {
            return;
        }
        PathIterator boundaryPathIterator = cellBoundaryPolygon.getPathIterator(null);
        double[] firstVertex = new double[2];
        double[] oldVertex = new double[2];
        double[] newVertex = new double[2];
        int segmentType = boundaryPathIterator.currentSegment(firstVertex);
        if (segmentType != PathIterator.SEG_MOVETO) {
            throw new AssertionError();
        }
        System.arraycopy(firstVertex, 0, newVertex, 0, 2);
        boundaryPathIterator.next();
        System.arraycopy(newVertex, 0, oldVertex, 0, 2);
        segmentType = boundaryPathIterator.currentSegment(newVertex);
        while (segmentType != PathIterator.SEG_CLOSE) {
            processSegment(oldVertex, newVertex);
            boundaryPathIterator.next();
            System.arraycopy(newVertex, 0, oldVertex, 0, 2);
            segmentType = boundaryPathIterator.currentSegment(newVertex);
        }
        processSegment(newVertex, firstVertex);
    }
    

    让我们花点时间快速查看initializeForNewCirclePrivate。此方法只是设置实例字段并允许派生类存储圆的任何属性。它的定义是

    private void initializeForNewCirclePrivate(Circle circle) {
        currentCircle = circle;
        currentSquareRadius = currentCircle.getRadius() * currentCircle.getRadius();
        initializeForNewCircle(circle);
    }
    

    initializeForNewCircle 是抽象的,一种实现是存储圆的半径以避免必须做平方根。无论如何回到processCircleShape。调用initializeForNewCirclePrivate 后,我们检查多边形是否为null(我将其解释为空多边形),如果它是null,则返回。在这种情况下,我们计算的面积将为零。如果多边形不是null,那么我们得到多边形的PathIterator。我调用的getPathIterator 方法的参数是可以应用于路径的仿射变换。不过我不想申请,所以我直接通过null

    接下来我声明将跟踪顶点的double[]s。我必须记住第一个顶点,因为PathIterator 只给了我每个顶点一次,所以我必须在它给我最后一个顶点后返回,并与最后一个顶点和第一个顶点形成一条边。

    下一行的currentSegment 方法将下一个顶点放入其参数中。它返回一个代码,告诉您它何时超出顶点。这就是为什么我的 while 循环的控制表达式是这样的原因。

    此方法的大部分其余代码都是与遍历顶点相关的无趣逻辑。重要的是,每次while循环的迭代我调用processSegment,然后在方法结束时再次调用processSegment,以处理将最后一个顶点连接到第一个顶点的边。

    我们来看processSegment的代码:

    private void processSegment(double[] initialVertex, double[] finalVertex) {
        double[] segmentDisplacement = displacment2D(initialVertex, finalVertex);
        if (segmentDisplacement[0] == 0 && segmentDisplacement[1] == 0) {
            return;
        }
        double segmentLength = Math.sqrt(dotProduct2D(segmentDisplacement, segmentDisplacement));
        double[] centerToInitialDisplacement = new double[]{initialVertex[0] - getCurrentCircle().getCenterX(), initialVertex[1] - getCurrentCircle().getCenterY()};
        final double leftX = dotProduct2D(centerToInitialDisplacement, segmentDisplacement) / segmentLength;
        final double rightX = leftX + segmentLength;
        final double y = wedgeProduct2D(segmentDisplacement, centerToInitialDisplacement) / segmentLength;
        processSegmentStandardGeometry(leftX, rightX, y);
    }
    

    在这种方法中,我实现了将边缘转换为上述标准几何图形的步骤。首先我计算segmentDisplacement,从初始顶点到最终顶点的位移。这定义了标准几何图形的 x 轴。如果这个位移为零,我会提前返回。

    接下来我计算位移的长度,因为这是得到x单位向量的必要条件。一旦我有了这些信息,我就会计算从圆心到初始顶点的位移。这个与segmentDisplacement 的点积给了我leftX,我一直称之为xi。然后rightX,我一直称之为xf,只是leftX + segmentLength。最后我做楔形产品得到y,如上所述。

    现在我已经将问题转化为标准几何,这将很容易处理。这就是processSegmentStandardGeometry 方法的作用。我们来看代码

    private void processSegmentStandardGeometry(double leftX, double rightX, double y) {
        if (y * y > getCurrentSquareRadius()) {
            processNonIntersectingRegion(leftX, rightX, y);
        } else {
            final double intersectionX = Math.sqrt(getCurrentSquareRadius() - y * y);
            if (leftX < -intersectionX) {
                final double leftRegionRightEndpoint = Math.min(-intersectionX, rightX);
                processNonIntersectingRegion(leftX, leftRegionRightEndpoint, y);
            }
            if (intersectionX < rightX) {
                final double rightRegionLeftEndpoint = Math.max(intersectionX, leftX);
                processNonIntersectingRegion(rightRegionLeftEndpoint, rightX, y);
            }
            final double middleRegionLeftEndpoint = Math.max(-intersectionX, leftX);
            final double middleRegionRightEndpoint = Math.min(intersectionX, rightX);
            final double middleRegionLength = Math.max(middleRegionRightEndpoint - middleRegionLeftEndpoint, 0);
            processIntersectingRegion(middleRegionLength, y);
        }
    }
    

    第一个if 区分y 小到足以使边缘与圆相交的情况。如果y很大,没有相交的可能,那么我调用该方法来处理这种情况。否则我会处理可能相交的情况。

    如果可能相交,我计算相交的 x 坐标intersectionX,并将边分成三部分,分别对应于上面标准几何图形的区域 1、2 和 3。首先我处理区域 1。

    为了处理区域 1,我检查 leftX 是否确实小于 -intersectionX,否则将没有区域 1。如果有区域 1,那么我需要知道它何时结束。它以rightX-intersectionX 的最小值结束。找到这些 x 坐标后,我将处理这个非相交区域。

    我做了类似的事情来处理区域 3。

    对于区域 2,我必须做一些逻辑来检查 leftXrightX 是否实际上将某个区域括在 -intersectionXintersectionX 之间。找到区域后,我只需要区域的长度和y,所以我将这两个数字传递给处理区域 2 的抽象方法。

    现在让我们看看processNonIntersectingRegion的代码

    private void processNonIntersectingRegion(double leftX, double rightX, double y) {
        final double initialTheta = Math.atan2(y, leftX);
        final double finalTheta = Math.atan2(y, rightX);
        double deltaTheta = finalTheta - initialTheta;
        if (deltaTheta < -Math.PI) {
            deltaTheta += 2 * Math.PI;
        } else if (deltaTheta > Math.PI) {
            deltaTheta -= 2 * Math.PI;
        }
        processNonIntersectingRegion(deltaTheta);
    }
    

    我只是使用atan2 来计算leftXrightX 之间的角度差。然后我添加代码来处理atan2 中的不连续性,但这可能是不必要的,因为不连续性发生在 180 度或 0 度。然后我将角度差异传递给抽象方法。最后,我们只有抽象方法和 getter:

        protected abstract void initialize();
    
        protected abstract void initializeForNewCircle(Circle circle);
    
        protected abstract void processNonIntersectingRegion(double deltaTheta);
    
        protected abstract void processIntersectingRegion(double length, double y);
    
        protected abstract T getValue();
    
        protected final Circle getCurrentCircle() {
            return currentCircle;
        }
    
        protected final double getCurrentSquareRadius() {
            return currentSquareRadius;
        }
    
    }
    

    现在让我们看看扩展类CircleAreaFinder

    public class CircleAreaFinder extends CircleShapeIntersectionFinder<Double> {
    
    public static double findAreaOfCircle(Circle circle, Shape shape) {
        CircleAreaFinder circleAreaFinder = new CircleAreaFinder();
        return circleAreaFinder.computeValue(circle, shape);
    }
    
    double area;
    
    @Override
    protected void initialize() {
        area = 0;
    }
    
    @Override
    protected void processNonIntersectingRegion(double deltaTheta) {
        area += getCurrentSquareRadius() * deltaTheta / 2;
    }
    
    @Override
    protected void processIntersectingRegion(double length, double y) {
        area -= length * y / 2;
    }
    
    @Override
    protected Double getValue() {
        return area;
    }
    
    @Override
    protected void initializeForNewCircle(Circle circle) {
    
    }
    

    }

    它有一个字段area 来跟踪该区域。 initialize 将区域设置为零,正如预期的那样。当我们处理非相交边时,我们将面积增加 R^2 Δθ/2,正如我们在上面得出的结论。对于相交边,我们将面积减小y*length/2。这样一来,y 的负值对应于正区域,正如我们决定的那样。

    现在最妙的是,如果我们想跟踪周界,我们不必做更多的工作。我定义了一个AreaPerimeter 类:

    public class AreaPerimeter {
    
        final double area;
        final double perimeter;
    
        public AreaPerimeter(double area, double perimeter) {
            this.area = area;
            this.perimeter = perimeter;
        }
    
        public double getArea() {
            return area;
        }
    
        public double getPerimeter() {
            return perimeter;
        }
    
    }
    

    现在我们只需要使用AreaPerimeter 作为类型再次扩展我们的抽象类。

    public class CircleAreaPerimeterFinder extends CircleShapeIntersectionFinder<AreaPerimeter> {
    
        public static AreaPerimeter findAreaPerimeterOfCircle(Circle circle, Shape shape) {
            CircleAreaPerimeterFinder circleAreaPerimeterFinder = new CircleAreaPerimeterFinder();
            return circleAreaPerimeterFinder.computeValue(circle, shape);
        }
    
        double perimeter;
        double radius;
        CircleAreaFinder circleAreaFinder;
    
        @Override
        protected void initialize() {
            perimeter = 0;
            circleAreaFinder = new CircleAreaFinder();
        }
    
        @Override
        protected void initializeForNewCircle(Circle circle) {
            radius = Math.sqrt(getCurrentSquareRadius());
        }
    
        @Override
        protected void processNonIntersectingRegion(double deltaTheta) {
            perimeter += deltaTheta * radius;
            circleAreaFinder.processNonIntersectingRegion(deltaTheta);
        }
    
        @Override
        protected void processIntersectingRegion(double length, double y) {
            perimeter += Math.abs(length);
            circleAreaFinder.processIntersectingRegion(length, y);
        }
    
        @Override
        protected AreaPerimeter getValue() {
            return new AreaPerimeter(circleAreaFinder.getValue(), perimeter);
        }
    
    }
    

    我们有一个变量perimeter 来跟踪周长,我们记住了radius 的值以避免大量调用Math.sqrt,我们将面积的计算委托给我们的CircleAreaFinder .我们可以看到周长的公式很简单。

    这里是CircleShapeIntersectionFinder的完整代码供参考

    private static double[] displacment2D(final double[] initialPoint, final double[] finalPoint) {
            return new double[]{finalPoint[0] - initialPoint[0], finalPoint[1] - initialPoint[1]};
        }
    
        private static double wedgeProduct2D(final double[] firstFactor, final double[] secondFactor) {
            return firstFactor[0] * secondFactor[1] - firstFactor[1] * secondFactor[0];
        }
    
        static private double dotProduct2D(final double[] firstFactor, final double[] secondFactor) {
            return firstFactor[0] * secondFactor[0] + firstFactor[1] * secondFactor[1];
        }
    
        private Circle currentCircle;
        private double currentSquareRadius;
    
        public final T computeValue(Circle circle, Shape shape) {
            initialize();
            processCircleShape(circle, shape);
            return getValue();
        }
    
        private void processCircleShape(Circle circle, final Shape cellBoundaryPolygon) {
            initializeForNewCirclePrivate(circle);
            if (cellBoundaryPolygon == null) {
                return;
            }
            PathIterator boundaryPathIterator = cellBoundaryPolygon.getPathIterator(null);
            double[] firstVertex = new double[2];
            double[] oldVertex = new double[2];
            double[] newVertex = new double[2];
            int segmentType = boundaryPathIterator.currentSegment(firstVertex);
            if (segmentType != PathIterator.SEG_MOVETO) {
                throw new AssertionError();
            }
            System.arraycopy(firstVertex, 0, newVertex, 0, 2);
            boundaryPathIterator.next();
            System.arraycopy(newVertex, 0, oldVertex, 0, 2);
            segmentType = boundaryPathIterator.currentSegment(newVertex);
            while (segmentType != PathIterator.SEG_CLOSE) {
                processSegment(oldVertex, newVertex);
                boundaryPathIterator.next();
                System.arraycopy(newVertex, 0, oldVertex, 0, 2);
                segmentType = boundaryPathIterator.currentSegment(newVertex);
            }
            processSegment(newVertex, firstVertex);
        }
    
        private void initializeForNewCirclePrivate(Circle circle) {
            currentCircle = circle;
            currentSquareRadius = currentCircle.getRadius() * currentCircle.getRadius();
            initializeForNewCircle(circle);
        }
    
        private void processSegment(double[] initialVertex, double[] finalVertex) {
            double[] segmentDisplacement = displacment2D(initialVertex, finalVertex);
            if (segmentDisplacement[0] == 0 && segmentDisplacement[1] == 0) {
                return;
            }
            double segmentLength = Math.sqrt(dotProduct2D(segmentDisplacement, segmentDisplacement));
            double[] centerToInitialDisplacement = new double[]{initialVertex[0] - getCurrentCircle().getCenterX(), initialVertex[1] - getCurrentCircle().getCenterY()};
            final double leftX = dotProduct2D(centerToInitialDisplacement, segmentDisplacement) / segmentLength;
            final double rightX = leftX + segmentLength;
            final double y = wedgeProduct2D(segmentDisplacement, centerToInitialDisplacement) / segmentLength;
            processSegmentStandardGeometry(leftX, rightX, y);
        }
    
        private void processSegmentStandardGeometry(double leftX, double rightX, double y) {
            if (y * y > getCurrentSquareRadius()) {
                processNonIntersectingRegion(leftX, rightX, y);
            } else {
                final double intersectionX = Math.sqrt(getCurrentSquareRadius() - y * y);
                if (leftX < -intersectionX) {
                    final double leftRegionRightEndpoint = Math.min(-intersectionX, rightX);
                    processNonIntersectingRegion(leftX, leftRegionRightEndpoint, y);
                }
                if (intersectionX < rightX) {
                    final double rightRegionLeftEndpoint = Math.max(intersectionX, leftX);
                    processNonIntersectingRegion(rightRegionLeftEndpoint, rightX, y);
                }
                final double middleRegionLeftEndpoint = Math.max(-intersectionX, leftX);
                final double middleRegionRightEndpoint = Math.min(intersectionX, rightX);
                final double middleRegionLength = Math.max(middleRegionRightEndpoint - middleRegionLeftEndpoint, 0);
                processIntersectingRegion(middleRegionLength, y);
            }
        }
    
        private void processNonIntersectingRegion(double leftX, double rightX, double y) {
            final double initialTheta = Math.atan2(y, leftX);
            final double finalTheta = Math.atan2(y, rightX);
            double deltaTheta = finalTheta - initialTheta;
            if (deltaTheta < -Math.PI) {
                deltaTheta += 2 * Math.PI;
            } else if (deltaTheta > Math.PI) {
                deltaTheta -= 2 * Math.PI;
            }
            processNonIntersectingRegion(deltaTheta);
        }
    
        protected abstract void initialize();
    
        protected abstract void initializeForNewCircle(Circle circle);
    
        protected abstract void processNonIntersectingRegion(double deltaTheta);
    
        protected abstract void processIntersectingRegion(double length, double y);
    
        protected abstract T getValue();
    
        protected final Circle getCurrentCircle() {
            return currentCircle;
        }
    
        protected final double getCurrentSquareRadius() {
            return currentSquareRadius;
        }
    

    无论如何,这就是我对算法的描述。我认为这很好,因为它是准确的,而且没有太多需要检查的案例。

    【讨论】:

    • 激烈的回答!我认为应该将它单独放在博客文章中
    • 我相信花费大量时间和精力来回答这个问题值得赞赏。这是我的。谢谢!
    【解决方案2】:

    如果您想要一个精确的解决方案(或至少与使用浮点算法一样精确),那么这将涉及大量的工作,因为要考虑的案例太多了。

    我统计了九种不同的情况(下图中按三角形在圆内的顶点数,以及与圆相交或包含在圆内的三角形的边数分类):

    (然而,众所周知,这种几何案例的枚举很棘手,如果我错过一两个,我一点也不感到惊讶!)

    所以方法是:

    1. 确定三角形的每个顶点是否在圆内。我假设你知道怎么做。

    2. 确定三角形的每条边是否与圆相交。 (我写了一种方法here,或查看任何计算几何书籍。)您需要计算一个或多个交点(如果有)以在步骤 4 中使用。

    3. 确定你有九个案例中的哪一个。

    4. 计算交叉点的面积。案例 1、2 和 9 很简单。在其余六个案例中,我绘制了虚线来展示如何根据三角形的原始顶点以及您在步骤 2 中计算的交点将相交区域划分为三角形和circular segments

    此算法将相当精细,并且容易出现仅影响其中一种情况的错误,因此请确保您有涵盖所有九种情况的测试用例(我建议也置换测试三角形的顶点)。特别注意三角形的一个顶点在圆的边缘上的情况。

    如果您不需要精确的解决方案,那么将图形栅格化并计算交叉点中的像素(正如其他几位受访者所建议的那样)似乎是一种更容易编写代码的方法,并且相应地更不容易出错。

    【讨论】:

    • +1 数学!似乎确切的解决方案也比光栅化技术运行得快得多。
    • 你的彻底性给我留下了深刻的印象。
    • 请注意,最简单的 #4 和 #5 方法是取圆的面积并减去三角形外部的线段(而不是将所有子三角形和内部的线段相加)。我真的很感动,Gareth。
    • 是的,这就是我没有细分这些案例的原因。您也可以通过从另一个段中减去一个段来完成案例 7。我认为任何实际实施这个东西的人都会非常清楚必要的剖析!
    • 亲爱的@Gareth,我正在考虑这个问题,以下观察结果可能与您的想法有关。问题归结为圆段面积计算 (SCAC)。不涉及其他可能的计算。换句话说,我相信(但不是 100% 肯定)以下观察是绝对正确的:在任何情况下,解决方案都可以写成基于 CSAC 的一些集合的加/减仅在三角形的线(经常延伸)上。继续...
    【解决方案3】:

    我迟到了将近一年半,但我想也许人们会对我写的code here 感兴趣,我认为这样做是正确的。查看底部附近的函数 IntersectionArea。一般的做法是把圆外接的凸多边形挑出来,再处理小圆帽。

    【讨论】:

      【解决方案4】:

      假设您说的是整数像素,而不是真实的,那么简单的实现是遍历三角形的每个像素,并检查圆心与半径之间的距离。

      这不是一个可爱的公式,也不是特别快,但它确实完成了工作。

      【讨论】:

        【解决方案5】:

        试试computational geometry

        注意:这不是小问题,希望不是功课;-)

        【讨论】:

          【解决方案6】:

          如果您有 GPU 可供使用,您可以使用this 技术来获取交叉点的像素数..

          【讨论】:

            【解决方案7】:

            我认为您不应该将圆形近似为一组三角形,而是可以用多边形近似它的形状。 朴素算法可能如下所示:

            1. 将圆转换为具有所需顶点数的多边形。
            2. 计算两个多边形(转换后的圆和三角形)的交点。
            3. 计算该交点的平方。

            您可以通过将第 2 步和第 3 步合并为单个函数来优化此算法。

            阅读此链接:
            Area of convex polygon
            Intersection of convex polygons

            【讨论】:

              【解决方案8】:

              由于您的形状是凸的,您可以使用蒙特卡洛面积估计。

              在圆形和三角形周围画一个框。

              在方框中选择随机点,并计算有多少落在圆圈中,有多少落在圆形和三角形中。

              相交面积 ≅ 圆面积 * # 圆和三角形中的点 / # 圆中的点

              当估计的面积在一定轮数内变化不超过一定量时停止选择点,或者只是根据框的面积选择固定数量的点。除非您的某个形状的面积很小,否则面积估计应该很快收敛。

              注意:确定点是否在三角形中的方法如下:Barycentric coordinates

              【讨论】:

                【解决方案9】:

                您需要精确到什么程度?如果您可以用更简单的形状近似圆形,则可以简化问题。例如,将圆建模为一组在中心相交的非常窄的三角形并不难。

                【讨论】:

                  【解决方案10】:

                  如果只有一个三角形的线段与圆相交,纯数学解决方案不会太难。一旦知道两个交点的时间,就可以使用距离公式求弦长。

                  根据these equations

                  ϑ = 2 sin⁻¹(0.5 c / r)
                  A = 0.5 r² (ϑ - sin(ϑ))
                  

                  其中 c 是弦长,r 是半径,ϑ 是通过中心的角度,A 是面积。请注意,如果超过一半的圆被切断,则此解决方案会中断。

                  如果您只需要一个近似值,这可能不值得付出努力,因为它对实际交叉点的样子做出了几个假设。

                  【讨论】:

                    【解决方案11】:

                    我的第一个直觉是转换一切,使圆以原点为中心,将三角形转换为极坐标,并求解三角形与圆的交点(或包围)。不过,我还没有真正在纸上完成它,所以这只是一种预感。

                    【讨论】:

                    • 我现在正在研究这种方法......在一般情况下,涉及一些相当丑陋的集成。我认为不会有一个计算机可以计算的简单的简单公式。
                    • 这感觉像是一些 19 世纪的数学家必须解决的问题,但不幸的是 Google Scholar 并没有追溯到那么远! =)
                    猜你喜欢
                    • 2015-07-29
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-01-09
                    • 2017-11-21
                    • 2023-03-24
                    • 2014-05-03
                    • 1970-01-01
                    • 2020-06-15
                    相关资源
                    最近更新 更多