Как вычислить площадь пересечения между треугольником (заданным как три пары (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)
{
 ...
}
20
Mark Maxham 12 Фев 2009 в 07:24

11 ответов

Лучший ответ

Если вам нужно точное решение (или, по крайней мере, настолько точное, насколько вы можете получить с помощью арифметики с плавающей запятой), это потребует много работы, потому что есть так много случаев, которые нужно учитывать.

Я насчитал девять различных случаев (на рисунке ниже они классифицированы по количеству вершин треугольника внутри круга и количеству ребер треугольника, которые пересекаются или содержатся в круге):

Nine cases for intersection: 1, 2. no vertices, no edges; 3. no vertices, one edge; 4. no vertices, two edges; 5. no vertices, three edges; 6. one vertex, two edges; 7. one vertex, three edges; 8. two vertices, three edges; 9. three vertices, three edges.

(Однако хорошо известно, что такое перечисление геометрических случаев сложно, и меня совсем не удивит, если я пропущу один или два!)

Итак, подход такой:

  1. Определите для каждой вершины треугольника, находится ли она внутри круга. Я предполагаю, что вы знаете, как это сделать.

  2. Определите для каждого края треугольника, пересекает ли он круг. (Я написал один метод здесь или посмотрите любую книгу по вычислительной геометрии .) Вам нужно будет вычислить точку или точки пересечения (если есть) для использования на шаге 4.

  3. Определите, какой из девяти случаев у вас есть.

  4. Вычислите площадь перекрестка. Случаи 1, 2 и 9 просты. В оставшихся шести случаях я нарисовал пунктирные линии, чтобы показать, как разделить область пересечения на треугольники и круговые сегменты. на основе исходных вершин треугольника и точек пересечения, вычисленных на шаге 2.

Этот алгоритм будет довольно деликатным и подвержен ошибкам, которые затрагивают только один из случаев, поэтому убедитесь, что у вас есть тестовые примеры, охватывающие все девять случаев (и я предлагаю также переставить вершины тестовых треугольников). Обратите особое внимание на случаи, когда одна из вершин треугольника находится на краю окружности.

Если вам не нужно точное решение, то растеризация фигур и подсчет пикселей на пересечении (как было предложено парой других респондентов) кажется гораздо более простым подходом к кодированию и, соответственно, менее подверженным ошибкам.

27
Gareth Rees 11 Фев 2015 в 17:29
+1 по математике! Похоже, что точное решение будет работать намного быстрее, чем растеризация techinque.
 – 
Crashworks
20 Фев 2009 в 01:23
1
Я впечатлен вашей тщательностью.
 – 
Mark Maxham
22 Фев 2009 в 08:05
Обратите внимание, что самый простой способ сделать №4 и №5 - взять площадь круга и вычесть сегменты за пределами треугольника (вместо того, чтобы складывать все подтреугольники и сегменты внутри него). Я действительно впечатлен, Гарет.
 – 
Crashworks
25 Фев 2009 в 00:20
Да, поэтому я не разделял эти случаи. Также вы можете выполнить случай 7, вычтя один сегмент из другого. Я думаю, что необходимые анализы будут совершенно ясны любому, кто действительно реализует эту штуку!
 – 
Gareth Rees
25 Фев 2009 в 02:53
1
Уважаемый @Gareth, я думал об этой проблеме, и, возможно, следующее наблюдение может иметь отношение к вашему размышлению по этому поводу. Проблема сводится к расчетам площади круговых сегментов (SCAC). Других возможных расчетов нет. Другими словами, я верю (но не уверен на 100%), что следующее наблюдение строго верно: в каждом случае решение может быть записано как сложение / вычитание некоторого набора CSAC на основе только на линиях (очень часто вытянутых) треугольника. Продолжение ...
 – 
Fattie
9 Дек 2011 в 16:20

Сначала я напомню, как найти площадь многоугольника. Как только мы это сделаем, алгоритм поиска пересечения между многоугольником и кругом должен быть легким для понимания.

Как найти площадь многоугольника

Давайте рассмотрим случай треугольника, потому что там проявляется вся необходимая логика. Предположим, у нас есть треугольник с вершинами (x1, y1), (x2, y2) и (x3, y3) при обходе треугольника против часовой стрелки, как показано на следующем рисунке: треугольникFigure

Затем вы можете вычислить площадь по формуле

А = (x1 y2 + x2 y3 + x3 y1 - x2y1- x3 y2 - x1y3) / 2.

Чтобы понять, почему эта формула работает, давайте изменим ее так, чтобы она была в форме

А = (x1 y2 - x2 y1) / 2 + (x2 y3 - x3 y2) / 2 + (x3 y1 - x1y3) / 2.

Теперь первый член - это следующая область, которая в нашем случае положительна: введите описание изображения здесь

Если неясно, действительно ли площадь зеленой области (x1 y2 - x2 y1) / 2, прочтите это .

Второй член - это область, которая снова положительна:

enter image description here

И третья область показана на следующем рисунке. На этот раз площадь отрицательная

enter image description here

Складывая эти три, мы получаем следующую картину

enter image description here

Мы видим, что зеленая область, которая была за пределами треугольника, отменяется красной областью, так что чистая площадь - это просто площадь треугольника, и это показывает, почему наша формула была верна в этом случае.

То, что я сказал выше, было интуитивным объяснением того, почему формула площади верна. Более строгое объяснение состоит в том, чтобы заметить, что когда мы вычисляем площадь от края, площадь, которую мы получаем, является той же площадью, которую мы получили бы от интегрирования r ^ 2dθ / 2, поэтому мы эффективно интегрируем r ^ 2dθ / 2 вокруг границы многоугольника, и по теореме Стокса это дает тот же результат, что и интегрирование rdrdθ по области, ограниченной многоугольником. Поскольку интегрирование rdrdθ по области, ограниченной многоугольником, дает площадь, мы заключаем, что наша процедура должна правильно определять площадь.

Площадь пересечения круга с многоугольником

Теперь давайте обсудим, как найти площадь пересечения круга радиуса R с многоугольником, как показано на следующем рисунке:

enter image description here

Нам интересно найти участок в зеленом районе. Мы можем, как и в случае с одним многоугольником, разбить наши вычисления на поиск области для каждой стороны многоугольника, а затем сложить эти области.

Наша первая область будет выглядеть так: введите описание изображения здесь

Вторая область будет иметь вид введите описание изображения здесь

И третья зона будет введите описание изображения здесь

Опять же, в нашем случае первые две области положительны, а третья - отрицательна. Надеюсь, отмена будет работать, так что чистая площадь действительно будет той областью, которая нам интересна. Посмотрим.

enter image description here

Действительно, сумма площадей будет областью, которая нас интересует.

Опять же, мы можем дать более строгое объяснение того, почему это работает. Пусть 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 по границе многоугольника.

Теперь, выполняя замену переменной, мы обнаруживаем, что мы получили бы тот же ответ, если бы мы проинтегрировали r ^ 2 dθ / 2 по границе π (P), где π (P) - образ P под π.

Снова используя теорему Стокса, мы знаем, что интегрирование r ^ 2 dθ / 2 по границе π (P) дает нам площадь π (P). Другими словами, он дает тот же ответ, что и интегрирование dxdy по π (P).

Снова используя замену переменной, мы знаем, что интегрирование dxdy по π (P) то же самое, что интегрирование Jdxdy по P, где J - якобиан π.

Теперь мы можем разделить интеграл Jdxdy на две области: часть в круге и часть за пределами круга. Теперь π оставляет только точки в круге, поэтому J = 1 там, поэтому вклад от этой части P - это площадь той части P, которая лежит в круге, то есть площадь пересечения. Вторая область - это область за пределами круга. Там J = 0, так как π схлопывает эту часть до границы круга.

Таким образом, мы действительно вычисляем площадь пересечения.

Теперь, когда мы относительно уверены, что концептуально знаем, как найти область, давайте поговорим более конкретно о том, как вычислить вклад от одного сегмента. Начнем с рассмотрения сегмента, который я назову «стандартной геометрией». Это показано ниже.

enter image description here

В стандартной геометрии кромка идет горизонтально слева направо. Он описывается тремя числами: 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. У Shape есть 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[], которые будут отслеживать вершины. Я должен запомнить первую вершину, потому что 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 мне нужно выполнить некоторую логику, чтобы проверить, действительно ли leftX и rightX ограничивают некоторую область между -intersectionX и intersectionX. После нахождения региона мне нужна только длина региона и 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 для вычисления разницы углов между leftX и rightX. Затем я добавляю код для устранения разрыва в atan2, но, вероятно, в этом нет необходимости, поскольку разрыв возникает либо под углом 180 градусов, либо под 0 градусов. Затем я передаю разницу в угле абстрактному методу. Наконец, у нас есть только абстрактные методы и геттеры:

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

Во всяком случае, это мое описание алгоритма. Я думаю, что это хорошо, потому что это точно, и на самом деле не так много случаев, которые нужно проверять.

36
Community 13 Апр 2017 в 15:19
5
Интенсивный ответ! Я думаю, это должно быть отдельно в сообщении в блоге
 – 
Lodewijk
27 Май 2014 в 01:28
2
Я считаю, что время и усилия, потраченные на то, чтобы дать этот ответ, заслуживают высокой оценки. А вот и мой. Спасибо!
 – 
Alp
3 Дек 2015 в 12:51

Предполагая, что вы говорите о целочисленных пикселях, а не о реальных, наивная реализация состояла бы в том, чтобы перебирать каждый пиксель треугольника и проверять расстояние от центра круга по его радиусу.

Это не изящная формула и не особенно быстрая, но она выполняет свою работу.

1
lc. 12 Фев 2009 в 07:31

Попробуйте вычислительную геометрию

Примечание: это нетривиальная проблема, надеюсь, это не домашнее задание ;-)

1
Steven A. Lowe 12 Фев 2009 в 07:35

Если в вашем распоряжении графический процессор, вы можете использовать этот метод получения количества пикселей на пересечении ..

1
Community 23 Май 2017 в 15:25

Я думаю, вам не следует приближать круг как набор треугольников, вместо этого вы можете приблизить его форму с помощью многоугольника. Наивный алгоритм может выглядеть так:

  1. Преобразуйте круг в многоугольник с желаемым количеством вершин.
  2. Вычислите пересечение двух многоугольников (преобразованный круг и треугольник).
  3. Вычислите квадрат этого пересечения.

Вы можете оптимизировать этот алгоритм, объединив шаги 2 и 3 в одну функцию.

Прочтите эти ссылки:
Площадь выпуклого многоугольника
Пересечение выпуклых многоугольников

1
okutane 12 Фев 2009 в 08:35

Поскольку ваши формы выпуклые, вы можете использовать оценку площади Монте-Карло.

Нарисуйте рамку вокруг круга и треугольника.

Выберите случайные точки в поле и подсчитайте, сколько из них попадает в круг, а сколько - как в круг, так и в треугольник.

Площадь пересечения ≅ Площадь круга * # точек в круге и треугольнике / # точек в круге

Прекратите выбирать точки, если предполагаемая площадь не изменилась более чем на определенную величину за определенное количество раундов, или просто выберите фиксированное количество точек в зависимости от площади поля. Оценка площади должна сходиться довольно быстро, если только одна из ваших фигур не имеет очень маленькую площадь.

Примечание. Вот как определить, находится ли точка в треугольнике: Барицентрические координаты

1
Imran 12 Фев 2009 в 12:17

Насколько точным вам нужно быть? Если вы можете аппроксимировать круг более простыми формами, вы можете упростить задачу. Например, нетрудно смоделировать круг как набор очень узких треугольников, пересекающихся в центре.

0
Mark Ransom 12 Фев 2009 в 07:52

Я опоздал почти на полтора года, но подумал, может быть, людям будет интересна код здесь, который я написал, который, как мне кажется, делает это правильно. Посмотрите функцию IntersectionArea внизу. Общий подход состоит в том, чтобы выделить выпуклый многоугольник, описанный кругом, а затем обработать маленькие круглые шляпки.

2
Victor Liu 19 Июн 2010 в 08:49

Если только один из отрезков треугольника пересекает круг, решение чистой математики не так уж сложно. Как только вы узнаете, когда находятся две точки пересечения, вы можете использовать формулу расстояния, чтобы найти длину хорды.

Согласно этим уравнениям:

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

Где c - длина хорды, r - радиус, ϑ - угол, проходящий через центр, а A - площадь. Обратите внимание, что это решение ломается, если отрезано более половины круга.

Вероятно, это не стоит усилий, если вам просто нужно приближение, поскольку оно делает несколько предположений о том, как выглядит фактическое пересечение.

0
Nikhil Chelliah 12 Фев 2009 в 08:09

Моим первым побуждением было бы преобразовать все так, чтобы круг был центрирован в начале координат, преобразовать треугольник в полярные координаты и найти пересечение (или охват) треугольника с кругом. Я еще не проработал это на бумаге, так что это только догадка.

0
Crashworks 12 Фев 2009 в 09:02
Я сейчас изучаю этот подход ... в общем случае здесь есть довольно уродливая интеграция. Я не думаю, что будет хорошая простая формула, которую может вычислить компьютер.
 – 
David Z
12 Фев 2009 в 09:06
2
Это похоже на то, что должно было быть разработано каким-то математиком 19-го века, но, к сожалению, Google Scholar не идет так далеко! знак равно
 – 
Crashworks
12 Фев 2009 в 09:12