Как определить, соответствует ли список точек полигона по часовой стрелке?

Имея список точек, как я могу найти, если они находятся в порядке по часовой стрелке?

Например:

point[0] = (5,0)
point[1] = (6,4)
point[2] = (4,5)
point[3] = (1,5)
point[4] = (1,0)

сказал бы, что он против часовой стрелки (или против часовой стрелки, для некоторых людей).

Ответ 1

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

Суммирование по ребрам (x 2 - x 1) (y 2 + y 1), Если результат положительный, кривая по часовой стрелке, если отрицательная кривая против часовой стрелки. (Результат дважды в закрытой области с соглашением +/-.)

point[0] = (5,0)   edge[0]: (6-5)(4+0) =   4
point[1] = (6,4)   edge[1]: (4-6)(5+4) = -18
point[2] = (4,5)   edge[2]: (1-4)(5+5) = -30
point[3] = (1,5)   edge[3]: (1-1)(0+5) =   0
point[4] = (1,0)   edge[4]: (5-1)(0+0) =   0
                                         ---
                                         -44  counter-clockwise

Ответ 2

Перекрестное произведение измеряет степень перпендикулярности двух векторов. Представьте себе, что каждое ребро вашего многоугольника является вектором в плоскости xy трехмерного пространства (3-D) xyz. Тогда поперечное произведение двух последовательных ребер является вектором в z-направлении (положительное z-направление, если второй отрезок по часовой стрелке, минус z-направление, если оно против часовой стрелки). Величина этого вектора пропорциональна синусу угла между двумя исходными ребрами, поэтому он достигает максимума, когда они перпендикулярны, и сужается, исчезая, когда ребра являются коллинеарными (параллельными).

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

Using your data:
point[0] = (5, 0)
point[1] = (6, 4)
point[2] = (4, 5)
point[3] = (1, 5)
point[4] = (1, 0)

Таким образом, последовательно повторяйте грани
edgeA - отрезок от point0 до point1 и
edgeB между point1 в point2
...
edgeE находится между point4 и point0.

Тогда вершина A (point0) находится между
edgeE [От point4 до point0 ]
edgeA [From point0 to 'point1'

Эти два ребра сами являются векторами, чьи координаты x и y могут быть определены путем вычитания координат их начальной и конечной точек:

edgeE= point0 - point4= (1, 0) - (5, 0)= (-4, 0) и
edgeA= point1 - point0= (6, 4) - (1, 0)= (5, 4) и

И перекрестное произведение этих двух смежных ребер вычисляется с использованием определителя следующей матрицы, который строится путем помещения координат двух векторов ниже символов, представляющих три координатные оси (i, j, & k). Третья (нулевая) -valued координата существует потому, что концепция перекрестного продукта является трехмерной конструкцией, поэтому мы расширяем эти двухмерные векторы в 3-D, чтобы применить кросс-произведение:

 i    j    k 
-4    0    0
 1    4    0    

Учитывая, что все кросс-продукты производят вектор, перпендикулярный плоскости двух векторов, умножаются, определитель указанной выше матрицы имеет только компонент k, (или z-оси).
Формула для вычисления величины компонента k или z-оси
a1*b2 - a2*b1 = -4* 4 - 0* 1= -16

Величина этого значения (-16) является мерой синуса угла между двумя исходными векторами, умноженными на произведение величин двух векторов.
На самом деле другая формула для ее значения
AXB (Cross Product) = |A| * |B| * sin(AB) AXB (Cross Product) = |A| * |B| * sin(AB).

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

|A| * |B| = 4 * Sqrt(17)= 16.4924...

Таким образом, мера sin (AB) = -16/16.4924= -.97014...

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

Сделайте это для каждого из 4-х точек вокруг замкнутого пути и добавьте значения из этого вычисления в каждой вершине.

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

Ответ 3

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

Вычислить подписанную область: A = 1/2 * (x 1 * y 2 - x 2 * y 1 + x 2 * y 3 - x 3 * y 2 +... + x n * y 1 - x 1 * y n)

Или в псевдокоде:

signedArea = 0
for each point in points:
    x1 = point[0]
    y1 = point[1]
    if point is last point
        x2 = firstPoint[0]
        y2 = firstPoint[1]
    else
        x2 = nextPoint[0]
        y2 = nextPoint[1]
    end if

    signedArea += (x1 * y2 - x2 * y1)
end for
return signedArea / 2

Обратите внимание, что если вы только проверяете заказ, вам не нужно делить на 2.

Источники: http://mathworld.wolfram.com/PolygonArea.html

Ответ 4

Найдите вершину с наименьшим y (и наибольшим x, если есть связи). Пусть вершина будет A а предыдущая вершина в списке будет B а следующая вершина в списке будет C Теперь вычислите знак перекрестного произведения AB и AC.


Рекомендации:

Ответ 5

Вот простая реализация алгоритма на С#, основанная на этом ответе.

Предположим, что у нас есть тип Vector имеющий свойства X и Y типа double.

public bool IsClockwise(IList<Vector> vertices)
{
    double sum = 0.0;
    for (int i = 0; i < vertices.Count; i++) {
        Vector v1 = vertices[i];
        Vector v2 = vertices[(i + 1) % vertices.Count];
        sum += (v2.X - v1.X) * (v2.Y + v1.Y);
    }
    return sum > 0.0;
}

% - это оператор по модулю или остатку, выполняющий операцию по модулю, которая (согласно Википедии) находит остаток после деления одного числа на другое.

Ответ 6

Начните с одной из вершин и вычислите угол, сложенный каждой стороной.

Первый и последний будут равны нулю (поэтому пропустите их); для остальных синус угла будет задаваться поперечным произведением нормировок на единицу длины (точка [n] -точка [0]) и (точка [n-1] -точка [0]).

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

Ответ 7

Для чего это стоит, я использовал этот mixin для расчета порядка намотки для приложений Google Maps API v3.

Код использует побочный эффект областей многоугольника: порядок вершин вершин по часовой стрелке дает положительную область, тогда как порядок намотки против часовой стрелки из тех же вершин создает ту же площадь, что и отрицательное значение. Код также использует своего рода частный API в геометрии Google Maps. Я чувствовал себя комфортно, используя его - используйте на свой страх и риск.

Использование образца:

var myPolygon = new google.maps.Polygon({/*options*/});
var isCW = myPolygon.isPathClockwise();

Полный пример с модульными тестами @http://jsfiddle.net/stevejansen/bq2ec/

/** Mixin to extend the behavior of the Google Maps JS API Polygon type
 *  to determine if a polygon path has clockwise of counter-clockwise winding order.
 *  
 *  Tested against v3.14 of the GMaps API.
 *
 *  @author  [email protected]
 *
 *  @license http://opensource.org/licenses/MIT
 *
 *  @version 1.0
 *
 *  @mixin
 *  
 *  @param {(number|Array|google.maps.MVCArray)} [path] - an optional polygon path; defaults to the first path of the polygon
 *  @returns {boolean} true if the path is clockwise; false if the path is counter-clockwise
 */
(function() {
  var category = 'google.maps.Polygon.isPathClockwise';
     // check that the GMaps API was already loaded
  if (null == google || null == google.maps || null == google.maps.Polygon) {
    console.error(category, 'Google Maps API not found');
    return;
  }
  if (typeof(google.maps.geometry.spherical.computeArea) !== 'function') {
    console.error(category, 'Google Maps geometry library not found');
    return;
  }

  if (typeof(google.maps.geometry.spherical.computeSignedArea) !== 'function') {
    console.error(category, 'Google Maps geometry library private function computeSignedArea() is missing; this may break this mixin');
  }

  function isPathClockwise(path) {
    var self = this,
        isCounterClockwise;

    if (null === path)
      throw new Error('Path is optional, but cannot be null');

    // default to the first path
    if (arguments.length === 0)
        path = self.getPath();

    // support for passing an index number to a path
    if (typeof(path) === 'number')
        path = self.getPaths().getAt(path);

    if (!path instanceof Array && !path instanceof google.maps.MVCArray)
      throw new Error('Path must be an Array or MVCArray');

    // negative polygon areas have counter-clockwise paths
    isCounterClockwise = (google.maps.geometry.spherical.computeSignedArea(path) < 0);

    return (!isCounterClockwise);
  }

  if (typeof(google.maps.Polygon.prototype.isPathClockwise) !== 'function') {
    google.maps.Polygon.prototype.isPathClockwise = isPathClockwise;
  }
})();

Ответ 8

Реализация ответа Шона в JavaScript:

function calcArea(poly) {
    if(!poly || poly.length < 3) return null;
    let end = poly.length - 1;
    let sum = poly[end][0]*poly[0][1] - poly[0][0]*poly[end][1];
    for(let i=0; i<end; ++i) {
        const n=i+1;
        sum += poly[i][0]*poly[n][1] - poly[n][0]*poly[i][1];
    }
    return sum;
}

function isClockwise(poly) {
    return calcArea(poly) > 0;
}

let poly = [[352,168],[305,208],[312,256],[366,287],[434,248],[416,186]];

console.log(isClockwise(poly));

let poly2 = [[618,186],[650,170],[701,179],[716,207],[708,247],[666,259],[637,246],[615,219]];

console.log(isClockwise(poly2));

Ответ 9

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

Ответ 10

Это реализованная функция для OpenLayers 2. Условие наличия полигона по часовой стрелке - это area < 0, это подтверждается этой ссылкой.

function IsClockwise(feature)
{
    if(feature.geometry == null)
        return -1;

    var vertices = feature.geometry.getVertices();
    var area = 0;

    for (var i = 0; i < (vertices.length); i++) {
        j = (i + 1) % vertices.length;

        area += vertices[i].x * vertices[j].y;
        area -= vertices[j].x * vertices[i].y;
        // console.log(area);
    }

    return (area < 0);
}

Ответ 11

Как также объясняется в этой статье в Википедии Ориентация кривой, учитывая 3 точки p, q и r на плоскости (т.е. С координатами x и y), вы можете вычислить знак следующего определителя

enter image description here

Если детерминант отрицательный (т.е. Orient(p, q, r) < 0), то многоугольник ориентирован по часовой стрелке (CW). Если определитель положителен (т. Orient(p, q, r) > 0), многоугольник ориентирован против часовой стрелки (CCW). Детерминант равен нулю (т. Orient(p, q, r) == 0), если точки p, q и r коллинеарны.

В приведенной выше формуле мы добавляем те, что находятся перед координатами p, q и r потому что мы используем однородные координаты.

Ответ 12

Это мое решение, используя объяснения в других ответах:

def segments(poly):
    """A sequence of (x,y) numeric coordinates pairs """
    return zip(poly, poly[1:] + [poly[0]])

def check_clockwise(poly):
    clockwise = False
    if (sum(x0*y1 - x1*y0 for ((x0, y0), (x1, y1)) in segments(poly))) < 0:
        clockwise = not clockwise
    return clockwise

poly = [(2,2),(6,2),(6,6),(2,6)]
check_clockwise(poly)
False

poly = [(2, 6), (6, 6), (6, 2), (2, 2)]
check_clockwise(poly)
True

Ответ 13

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

Ответ 14

Решение My С#/LINQ основано на совете по использованию кросс-продукта @charlesbretana ниже. Вы можете указать нормальное значение для обмотки. Он должен работать до тех пор, пока кривая в основном находится в плоскости, определяемой вектором вверх.

using System.Collections.Generic;
using System.Linq;
using System.Numerics;

namespace SolidworksAddinFramework.Geometry
{
    public static class PlanePolygon
    {
        /// <summary>
        /// Assumes that polygon is closed, ie first and last points are the same
        /// </summary>
       public static bool Orientation
           (this IEnumerable<Vector3> polygon, Vector3 up)
        {
            var sum = polygon
                .Buffer(2, 1) // from Interactive Extensions Nuget Pkg
                .Where(b => b.Count == 2)
                .Aggregate
                  ( Vector3.Zero
                  , (p, b) => p + Vector3.Cross(b[0], b[1])
                                  /b[0].Length()/b[1].Length());

            return Vector3.Dot(up, sum) > 0;

        } 

    }
}

с unit test

namespace SolidworksAddinFramework.Spec.Geometry
{
    public class PlanePolygonSpec
    {
        [Fact]
        public void OrientationShouldWork()
        {

            var points = Sequences.LinSpace(0, Math.PI*2, 100)
                .Select(t => new Vector3((float) Math.Cos(t), (float) Math.Sin(t), 0))
                .ToList();

            points.Orientation(Vector3.UnitZ).Should().BeTrue();
            points.Reverse();
            points.Orientation(Vector3.UnitZ).Should().BeFalse();



        } 
    }
}

Ответ 15

После тестирования нескольких ненадежных реализаций алгоритм, обеспечивающий удовлетворительные результаты в отношении ориентации CW/CCW из коробки, был тем, который был опубликован OP в этом потоке (shoelace_formula_3).

Как всегда, положительное число представляет собой ориентацию CW, тогда как отрицательное число CCW.

Ответ 16

Многопроцессорный метод проще, если вы уже знаете точку внутри полигона:

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

  2. Добавьте известную "внутреннюю" точку и сформируйте треугольник.

  3. Вычислите CW или CCW, как предлагается здесь, с этими тремя пунктами.

Ответ 17

Здесь быстрое решение 3.0, основанное на ответах выше:

    for (i, point) in allPoints.enumerated() {
        let nextPoint = i == allPoints.count - 1 ? allPoints[0] : allPoints[i+1]
        signedArea += (point.x * nextPoint.y - nextPoint.x * point.y)
    }

    let clockwise  = signedArea < 0

Ответ 18

Другое решение для этого;

const isClockwise = (vertices=[]) => {
    const len = vertices.length;
    const sum = vertices.map(({x, y}, index) => {
        let nextIndex = index + 1;
        if (nextIndex === len) nextIndex = 0;

        return {
            x1: x,
            x2: vertices[nextIndex].x,
            y1: x,
            y2: vertices[nextIndex].x
        }
    }).map(({ x1, x2, y1, y2}) => ((x2 - x1) * (y1 + y2))).reduce((a, b) => a + b);

    if (sum > -1) return true;
    if (sum < 0) return false;
}

Возьмите все вершины как массив, подобный этому;

const vertices = [{x: 5, y: 0}, {x: 6, y: 4}, {x: 4, y: 5}, {x: 1, y: 5}, {x: 1, y: 0}];
isClockwise(vertices);

Ответ 19

Решение для R определить направление и обратное, если по часовой стрелке (это необходимо для объектов owin):

coords <- cbind(x = c(5,6,4,1,1),y = c(0,4,5,5,0))
a <- numeric()
for (i in 1:dim(coords)[1]){
  #print(i)
  q <- i + 1
  if (i == (dim(coords)[1])) q <- 1
  out <- ((coords[q,1]) - (coords[i,1])) * ((coords[q,2]) + (coords[i,2]))
  a[q] <- out
  rm(q,out)
} #end i loop

rm(i)

a <- sum(a) #-ve is anti-clockwise

b <- cbind(x = rev(coords[,1]), y = rev(coords[,2]))

if (a>0) coords <- b #reverses coords if polygon not traced in anti-clockwise direction

Ответ 20

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

точка [0] = (5,0)

точка [1] = (6,4)

точка [2] = (4,5)

балл [3] = (1,5)

точка [4] = (1,0)

Если мы предположим, что P2 - самая северная точка, то восточная точка либо предыдущей, либо следующей точки определяет по часовой стрелке, CW или CCW. Поскольку самая северная точка находится на северной стороне, если P1 (предыдущий) к P2 перемещается на восток, направление - CW. В этом случае он движется на запад, поэтому в направлении принятого ответа указано CCW. Если предыдущая точка не имеет горизонтального перемещения, то та же система применяется к следующей точке, P3. Если P3 к западу от P2, это так, тогда движение - это против часовой стрелки. Если движение P2 к P3 - восток, в этом случае это запад, движение CW. Предположим, что nte, P2 в ваших данных, является самой северной, чем восточной точкой, а prv - предыдущей точкой, P1 в ваших данных, а nxt - следующей точкой, P3 в ваших данных, а [0] - горизонтальная или восточная/запад, где запад меньше, чем восток, а [1] вертикальный.

if (nte[0] >= prv[0] && nxt[0] >= nte[0]) return(CW);
if (nte[0] <= prv[0] && nxt[0] <= nte[0]) return(CCW);
// Okay, it not easy-peasy, so now, do the math
if (nte[0] * nxt[1] - nte[1] * nxt[0] - prv[0] * (nxt[1] - crt[1]) + prv[1] * (nxt[0] - nte[0]) >= 0) return(CCW); // For quadrant 3 return(CW)
return(CW) // For quadrant 3 return (CCW)

Ответ 21

Код С# для реализации ответа LHF:

// https://en.wikipedia.org/wiki/Curve_orientation#Orientation_of_a_simple_polygon
public static WindingOrder DetermineWindingOrder(IList<Vector2> vertices)
{
    int nVerts = vertices.Count;
    // If vertices duplicates first as last to represent closed polygon,
    // skip last.
    Vector2 lastV = vertices[nVerts - 1];
    if (lastV.Equals(vertices[0]))
        nVerts -= 1;
    int iMinVertex = FindCornerVertex(vertices);
    // Orientation matrix:
    //     [ 1  xa  ya ]
    // O = | 1  xb  yb |
    //     [ 1  xc  yc ]
    Vector2 a = vertices[WrapAt(iMinVertex - 1, nVerts)];
    Vector2 b = vertices[iMinVertex];
    Vector2 c = vertices[WrapAt(iMinVertex + 1, nVerts)];
    // determinant(O) = (xb*yc + xa*yb + ya*xc) - (ya*xb + yb*xc + xa*yc)
    double detOrient = (b.X * c.Y + a.X * b.Y + a.Y * c.X) - (a.Y * b.X + b.Y * c.X + a.X * c.Y);

    // TBD: check for "==0", in which case is not defined?
    // Can that happen?  Do we need to check other vertices / eliminate duplicate vertices?
    WindingOrder result = detOrient > 0
            ? WindingOrder.Clockwise
            : WindingOrder.CounterClockwise;
    return result;
}

public enum WindingOrder
{
    Clockwise,
    CounterClockwise
}

// Find vertex along one edge of bounding box.
// In this case, we find smallest y; in case of tie also smallest x.
private static int FindCornerVertex(IList<Vector2> vertices)
{
    int iMinVertex = -1;
    float minY = float.MaxValue;
    float minXAtMinY = float.MaxValue;
    for (int i = 0; i < vertices.Count; i++)
    {
        Vector2 vert = vertices[i];
        float y = vert.Y;
        if (y > minY)
            continue;
        if (y == minY)
            if (vert.X >= minXAtMinY)
                continue;

        // Minimum so far.
        iMinVertex = i;
        minY = y;
        minXAtMinY = vert.X;
    }

    return iMinVertex;
}

// Return value in (0..n-1).
// Works for i in (-n..+infinity).
// If need to allow more negative values, need more complex formula.
private static int WrapAt(int i, int n)
{
    // "+n": Moves (-n..) up to (0..).
    return (i + n) % n;
}

Ответ 22

найти центр масс этих точек.

предположим, что в этой точке есть линии от этой точки.

найдите угол между двумя линиями для линии line1

чем сделать это для строк1 и строки2

...

...

если этот угол монотонно возрастает, чем против часовой стрелки,

else, если монотонно уменьшить его по часовой стрелке

else (это не монотонно)

вы не можете решить, так что это не мудро