web-dev-qa-db-fra.com

Comment déterminer si une liste de points de polygones est dans le sens des aiguilles d'une montre?

Ayant une liste de points, comment puis-je trouver s’ils sont dans le sens des aiguilles d’une montre?

Par exemple:

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

dirait que c'est anti-horaire (ou anti-horaire, pour certaines personnes).

225
Stécy

Certaines des méthodes suggérées échoueront dans le cas d'un polygone non convexe, tel qu'un croissant. Voici un exemple simple qui fonctionnera avec des polygones non convexes (il fonctionnera même avec un polygone à intersection identique à celle d’un chiffre huit), vous indiquant si la plupart dans le sens des aiguilles d'une montre).

Somme sur les bords, (x2 - x1) (y2 + y1). Si le résultat est positif, la courbe est dans le sens des aiguilles d'une montre, si elle est négative, la courbe est dans le sens inverse. (Le résultat est deux fois la zone incluse, avec une convention +/-.)

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
368
Beta

Le produit croisé mesure le degré de perpendicularité de deux vecteurs. Imaginez que chaque bord de votre polygone soit un vecteur dans le plan x-y d'un espace xyz tridimensionnel (3-D). Ensuite, le produit transversal de deux arêtes successives est un vecteur dans la direction z, (direction z positive si le deuxième segment est dans le sens des aiguilles d'une montre, moins direction z si elle est dans le sens inverse des aiguilles d'une montre). La magnitude de ce vecteur est proportionnelle au sinus de l'angle entre les deux arêtes d'origine; elle atteint donc un maximum lorsqu'elles sont perpendiculaires et s'efface pour disparaître lorsque les arêtes sont colinéaires (parallèles).

Ainsi, pour chaque sommet (point) du polygone, calculez la magnitude de produit croisé des deux arêtes adjacentes: 

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

Alors étiquetez les bords consécutivement comme
edgeA est le segment de point0 à point1 et
edgeB entre point1 à point2
...
edgeE est entre point4 et point0

Alors le sommet A (point0) est compris entre
edgeE [De point4 à point0]
edgeA [De point0 à 'point1' 

Ces deux arêtes sont elles-mêmes des vecteurs, dont les coordonnées x et y peuvent être déterminées en soustrayant les coordonnées de leurs points de départ et d'arrivée:

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

Et le produit croisé de ces deux arêtes adjacentes est calculé à l'aide du déterminant de la matrice suivante, qui est construite en plaçant les coordonnées des deux vecteurs sous les symboles représentant les trois axes de coordonnées (i, j et k). La troisième coordonnée (zéro) est présente car le concept de produit croisé est une construction 3D, nous étendons donc ces vecteurs 2D en 3D afin d'appliquer le produit croisé:

 i    j    k 
-4    0    0
 1    4    0    

Étant donné que tous les produits croisés produisent un vecteur perpendiculaire au plan de la multiplication de deux vecteurs, le déterminant de la matrice ci-dessus n'a qu'une composante k (ou axe z).
La formule de calcul de la grandeur de la composante k ou z est la suivante:
a1*b2 - a2*b1 = -4* 4 - 0* 1 = -16

La magnitude de cette valeur (-16) est une mesure du sinus de l'angle entre les 2 vecteurs d'origine, multipliée par le produit des magnitudes des 2 vecteurs.
En fait, une autre formule pour sa valeur est
A X B (Cross Product) = |A| * |B| * sin(AB)

Donc, pour revenir à une mesure de l'angle, vous devez diviser cette valeur, (-16), par le produit de la magnitude des deux vecteurs. 

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

Donc, la mesure du péché (AB) = -16 / 16.4924 = -.97014...

Il s'agit d'une mesure indiquant si le prochain segment après le sommet a été courbé vers la gauche ou la droite et de combien. Il n'y a pas besoin de prendre arc-sinus. Nous ne nous soucions que de son ampleur et bien sûr de son signe (positif ou négatif)!

Faites cela pour chacun des 4 autres points autour du tracé fermé et additionnez les valeurs de ce calcul à chaque sommet. 

Si la somme finale est positive, vous êtes allé dans le sens horaire, négatif, dans le sens antihoraire.

49
Charles Bretana

Je suppose que la question est assez ancienne, mais je vais quand même proposer une autre solution, car elle est simple et peu mathématique - elle utilise uniquement une algèbre de base. Calcule l'aire signée du polygone. Si elle est négative, les points sont dans le sens des aiguilles d'une montre, si elle est positive, ils le sont dans le sens inverse des aiguilles d'une montre. (Ceci est très similaire à la solution de Beta.)

Calculez la surface signée: A = 1/2 * (x1* y2 - X2* y1 + x2* y3 - X3* y2 + ... + xn* y1 - X1* yn)

Ou en pseudo-code:

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

Notez que si vous ne faites que vérifier la commande, vous n'avez pas besoin de diviser par 2.

Sources: http://mathworld.wolfram.com/PolygonArea.html

43
Sean the Bean

Voici une simple implémentation de l'algorithme en C # basée sur cette réponse .

Supposons que nous ayons un type Vector ayant les propriétés X et Y de type 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]; // % is the modulo operator
        sum += (v2.X - v1.X) * (v2.Y + v1.Y);
    }
    return sum > 0.0;
}
20

Trouvez le sommet avec le plus petit y (et le plus grand x s'il y a des liens). Laissez le sommet être A et le sommet précédent dans la liste soit B et le prochain sommet dans la liste soit C. Maintenant, calculez le signe du produit croisé de AB et AC.


Références:

17
lhf

Commencez par l’un des sommets et calculez l’angle sous-tendu de chaque côté. 

Le premier et le dernier seront zéro (donc sautez ceux-ci); pour le reste, le sinus de l'angle sera donné par le produit croisé des normalisations en unités de longueur de (point [n] -point [0]) et (point [n-1] -point [0]).

Si la somme des valeurs est positive, votre polygone est tracé dans le sens inverse des aiguilles d'une montre.

6
Steve Gilham

Une implémentation de la réponse de Sean en 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));

Je suis sûr que c'est vrai. Cela semble fonctionner :-)

Ces polygones ressemblent à ceci, si vous vous demandez:

4
mpen

Pour ce que cela vaut, j’ai utilisé ce mixin pour calculer l’ordre d’enroulement des applications Google Maps API v3.

Le code exploite l'effet secondaire des zones polygonales: un ordre d'enroulement des sommets dans le sens des aiguilles d'une montre génère une zone positive, tandis qu'un ordre d'enroulement dans le sens inverse des mêmes sommets produit la même surface qu'une valeur négative. Le code utilise également une sorte d'API privée dans la bibliothèque de géométrie de Google Maps. Je me sentais à l'aise de l'utiliser - utilisez-le à vos risques et périls.

Exemple d'utilisation:

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

Exemple complet avec des tests unitaires @ 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;
  }
})();
4
Steve Jansen

C'est la fonction implémentée pour OpenLayers 2 . La condition pour avoir un polygone dans le sens horaire est area < 0, cela est confirmé par cette référence .

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

Si vous utilisez Matlab, la fonction ispolycw renvoie true si les sommets du polygone sont dans le sens des aiguilles d'une montre.

2
Frederick

Comme expliqué également dans cet article de Wikipédia Orientation de la courbe , à partir de 3 points p, q et r sur le plan (c'est-à-dire avec les coordonnées x et y), vous pouvez calculer le signe du déterminant suivant

enter image description here

Si le déterminant est négatif (c'est-à-dire Orient(p, q, r) < 0), le polygone est alors orienté dans le sens des aiguilles d'une montre (CW). Si le déterminant est positif (c'est-à-dire Orient(p, q, r) > 0), le polygone est orienté dans le sens inverse des aiguilles d'une montre (CCW). Le déterminant est zéro (c'est-à-dire Orient(p, q, r) == 0) si les points p, q et r sont colinéaires .

Dans la formule ci-dessus, nous précédons ceux précédant les coordonnées p, q et r car nous utilisons coordonnées homogènes .

1
Ian

Solution pour que R détermine la direction et inverse dans le sens des aiguilles d'une montre (jugé nécessaire pour les objets 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
0
dez

Je pense que pour que certains points soient donnés dans le sens des aiguilles d'une montre, tous les bords doivent être positifs et pas seulement la somme des bords. Si un bord est négatif, au moins 3 points sont donnés dans le sens anti-horaire.

0
daniel

Voici la solution Swift 3.0 basée sur les réponses ci-dessus:

    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
0
Toby Evetts

Une autre solution pour cela;

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

Prenez tous les sommets comme un tableau comme celui-ci;

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

Ceci est ma solution en utilisant les explications dans les autres réponses:

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
0
Gianni Spear

Bien que ces réponses soient correctes, elles sont mathématiquement plus intenses que nécessaire. Supposons les coordonnées de la carte, où le point le plus au nord est le point le plus haut de la carte. Trouve le point le plus au nord, et si 2 points sont égaux, c’est le plus au nord puis le plus à l’est (c’est le point que lhf utilise dans sa réponse). Dans vos points,

point [0] = (5,0)

point [1] = (6,4)

point [2] = (4,5)

point [3] = (1,5)

point [4] = (1,0)

Si nous supposons que P2 est le plus au nord, le point est, soit le point précédent, soit le point suivant, détermine dans le sens des aiguilles d'une montre, CW ou CCW. Comme le point le plus au nord se trouve sur la face nord, si le P1 (précédent) vers le P2 se déplace vers l’est, la direction est CW. Dans ce cas, il se déplace vers l'ouest, la direction est donc CCW comme le dit la réponse acceptée. Si le point précédent n'a pas de mouvement horizontal, le même système s'applique au point suivant, P3. Si P3 est à l'ouest de P2, c'est le cas, alors le mouvement est CCW. Si le mouvement P2 à P3 est à l'est, c'est à l'ouest dans ce cas, le mouvement est CW. Supposons que nte, P2 dans vos données, soit le point le plus au nord puis à l'est et que prv soit le point précédent, P1 dans vos données et que nxt soit le point suivant, P3 dans vos données et que [0] soit horizontal ou est/ouest où ouest est inférieur à l'est et [1] est vertical.

if (nte[0] >= prv[0] && nxt[0] >= nte[0]) return(CW);
if (nte[0] <= prv[0] && nxt[0] <= nte[0]) return(CCW);
// Okay, it's 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)
0
VectorVortec

Après avoir testé plusieurs implémentations peu fiables, l’algorithme fournissant des résultats satisfaisants concernant l’orientation CW/CCW était celui posté par OP dans this thread (shoelace_formula_3). 

Comme toujours, un nombre positif représente une orientation CW, alors qu'un nombre négatif CCW.

0
Marjan Moderc

Code C # pour implémenter réponse de 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;
}
0
ToolmakerSteve

Une méthode beaucoup plus simple, si vous connaissez déjà un point à l'intérieur du polygone:

  1. Choisissez n'importe quel segment de ligne du polygone d'origine, des points et de leurs coordonnées dans cet ordre.

  2. Ajoutez un point "intérieur" connu et formez un triangle.

  3. Calculez CW ou CCW comme suggéré ici avec ces trois points.

0
Venkata Goli

La solution My C #/LINQ est basée sur les conseils multiservices de @charlesbretana ci-dessous. Vous pouvez spécifier une normale de référence pour le bobinage. Cela devrait fonctionner aussi longtemps que la courbe est principalement dans le plan défini par le vecteur haut.

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;

        } 

    }
}

avec un test unitaire

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



        } 
    }
}
0
bradgonesurfing