Avendo un elenco di punti, come trovo se sono in senso orario?
Per esempio:
point[0] = (5,0)
point[1] = (6,4)
point[2] = (4,5)
point[3] = (1,5)
point[4] = (1,0)
direi che è antiorario (o antiorario, per alcune persone).
Avendo un elenco di punti, come trovo se sono in senso orario?
Per esempio:
point[0] = (5,0)
point[1] = (6,4)
point[2] = (4,5)
point[3] = (1,5)
point[4] = (1,0)
direi che è antiorario (o antiorario, per alcune persone).
Risposte:
Alcuni dei metodi suggeriti falliranno nel caso di un poligono non convesso, come una mezzaluna. Eccone uno semplice che funzionerà con poligoni non convessi (funzionerà anche con un poligono autointersecante come una figura otto, che ti dice se è principalmente in senso orario).
Somma sopra i bordi, (x 2 - x 1 ) (y 2 + y 1 ). Se il risultato è positivo, la curva è in senso orario, se è negativa la curva è in senso antiorario. (Il risultato è il doppio dell'area racchiusa, con una convenzione +/-.)
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
Sum( (x[(i+1) mod N] - x[i]) * (y[i] + y[(i+1) mod N]) )
per i = da 0 a N-1. Vale a dire, deve prendere l'indice Modulo N ( N ≡ 0
) La formula funziona solo per poligoni chiusi . I poligoni non hanno bordi immaginari.
Il prodotto incrociato misura il grado di perpendicolarità di due vettori. Immagina che ogni bordo del tuo poligono sia un vettore nel piano xy di uno spazio xyz tridimensionale (3-D). Quindi il prodotto incrociato di due bordi successivi è un vettore nella direzione z (direzione z positiva se il secondo segmento è in senso orario, meno direzione z se è in senso antiorario). La grandezza di questo vettore è proporzionale al seno dell'angolo tra i due bordi originali, quindi raggiunge il massimo quando sono perpendicolari e si assottiglia per scomparire quando i bordi sono collineari (paralleli).
Quindi, per ciascun vertice (punto) del poligono, calcola l'entità del prodotto incrociato dei due bordi adiacenti:
Using your data:
point[0] = (5, 0)
point[1] = (6, 4)
point[2] = (4, 5)
point[3] = (1, 5)
point[4] = (1, 0)
Quindi etichettare i bordi consecutivamente così come
edgeA
il segmento da point0
a point1
e
edgeB
tra point1
a point2
...
edgeE
è tra point4
e point0
.
Quindi il vertice A ( point0
) è compreso tra
edgeE
[Da point4
a point0
]
edgeA
[Da point0
a `punto1 '
Questi due bordi sono essi stessi vettori, le cui coordinate xey possono essere determinate sottraendo le coordinate dei loro punti iniziale e finale:
edgeE
= point0
- point4
= (1, 0) - (5, 0)
= (-4, 0)
e
edgeA
= point1
- point0
= (6, 4) - (1, 0)
= (5, 4)
e
E il prodotto vettoriale di questi due bordi contigui è calcolato utilizzando il determinante della matrice seguente, che è costruito inserendo le coordinate dei due vettori sotto i simboli che rappresentano i tre assi coordinati ( i
, j
, e k
). La terza coordinata valutata (zero) è lì perché il concetto di prodotto incrociato è un costrutto 3D, quindi estendiamo questi vettori 2D in 3D per applicare il prodotto incrociato:
i j k
-4 0 0
1 4 0
Dato che tutti i prodotti incrociati producono un vettore perpendicolare al piano di due vettori che vengono moltiplicati, il determinante della matrice sopra ha solo un componente k
, (o asse z).
La formula per calcolare la grandezza del k
componente dell'asse z è
a1*b2 - a2*b1 = -4* 4 - 0* 1
= -16
La grandezza di questo valore ( -16
), è una misura del seno dell'angolo tra i 2 vettori originali, moltiplicato per il prodotto delle magnitudini dei 2 vettori.
In realtà, un'altra formula per il suo valore è
A X B (Cross Product) = |A| * |B| * sin(AB)
.
Quindi, per tornare a solo una misura dell'angolo è necessario dividere questo valore, ( -16
), per il prodotto delle magnitudini dei due vettori.
|A| * |B|
= 4 * Sqrt(17)
=16.4924...
Quindi la misura del peccato (AB) = -16 / 16.4924
=-.97014...
Questa è una misura per stabilire se il segmento successivo dopo il vertice si è piegato a sinistra o a destra, e di quanto. Non è necessario assumere arc-seno. Tutto ciò che ci interesserà è la sua grandezza, e ovviamente il suo segno (positivo o negativo)!
Fallo per ciascuno degli altri 4 punti attorno al percorso chiuso e aggiungi i valori di questo calcolo in ciascun vertice.
Se la somma finale è positiva, sei andato in senso orario, negativo, in senso antiorario.
Immagino che questa sia una domanda piuttosto vecchia, ma proverò comunque un'altra soluzione, perché è semplice e non matematicamente intensiva: utilizza solo l'algebra di base. Calcola l'area firmata del poligono. Se è negativo i punti sono in senso orario, se è positivo sono in senso antiorario. (Questo è molto simile alla soluzione Beta.)
Calcola l'area firmata: 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 )
O in pseudo-codice:
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
Nota che se stai solo controllando l'ordinamento, non devi preoccuparti di dividere per 2.
previousPoint
per la prossima iterazione. Prima di iniziare il loop, impostare previousPoint
sull'ultimo punto dell'array. Il compromesso è una copia variabile locale aggiuntiva ma un numero inferiore di accessi agli array. E, soprattutto, non è necessario toccare l'array di input.
Trova il vertice con la più piccola y (e la più grande x se ci sono legami). Lascia che sia il vertice A
e il vertice precedente nell'elenco B
e il vertice successivo nell'elenco C
. Ora calcola il segno del prodotto incrociato di AB
e AC
.
Riferimenti:
Come trovo l'orientamento di un semplice poligono? in Domande frequenti: comp.graphics.algorithms .
Orientamento della curva su Wikipedia.
O(1)
soluzione. Tutte le altre risposte forniscono O(n)
soluzioni per n
il numero di punti poligonali. Per ottimizzazioni ancora più profonde, consultare la sottosezione Considerazioni pratiche del fantastico articolo di orientamento Curve di Wikipedia .
O(1)
solo se (A) questo poligono è convesso (nel qual caso qualsiasi vertice arbitrario risiede sullo scafo convesso e quindi è sufficiente) o (B) si conosce già il vertice con la coordinata Y più piccola. In caso contrario (ovvero, questo poligono non è convesso e non ne sai nulla),O(n)
è necessariaunaricerca. Poiché non è richiesta alcuna somma, tuttavia, questo è ancora notevolmente più veloce di qualsiasi altra soluzione per poligoni semplici.
Ecco una semplice implementazione in C # dell'algoritmo basata su questa risposta .
Supponiamo di avere un Vector
tipo con X
e Y
proprietà di tipo 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;
}
%
è l'operatore modulo o resto che esegue l'operazione modulo che ( secondo Wikipedia ) trova il resto dopo la divisione di un numero per un altro.
Inizia da uno dei vertici e calcola l'angolo sotteso da ciascun lato.
Il primo e l'ultimo saranno zero (quindi salta quelli); per il resto, il seno dell'angolo sarà dato dal prodotto incrociato delle normalizzazioni alla lunghezza unitaria di (punto [n]-punto [0]) e (punto [n-1]-punto [0]).
Se la somma dei valori è positiva, il poligono viene disegnato in senso antiorario.
Per quello che vale, ho usato questo mixin per calcolare l'ordine degli avvolgimenti per le app v3 dell'API di Google Maps.
Il codice sfrutta l'effetto collaterale delle aree poligonali: un ordine di avvolgimento in senso orario di vertici produce un'area positiva, mentre un ordine di avvolgimento in senso antiorario degli stessi vertici produce la stessa area di un valore negativo. Il codice utilizza anche una sorta di API privata nella libreria geometrica di Google Maps. Mi sentivo a mio agio nell'usarlo - usare a proprio rischio.
Esempio di utilizzo:
var myPolygon = new google.maps.Polygon({/*options*/});
var isCW = myPolygon.isPathClockwise();
Esempio completo con unit test @ 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 stevejansen_github@icloud.com
*
* @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;
}
})();
Un'implementazione della risposta di Sean in 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));
Abbastanza sicuro che sia giusto. Sembra funzionare :-)
Quei poligoni sembrano così, se ti stai chiedendo:
Questa è la funzione implementata per OpenLayers 2 . La condizione per avere un poligono in senso orario è area < 0
, confermata da questo riferimento .
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);
}
Come anche illustrato in questo articolo Wikipedia orientamento curva , in 3 punti p
, q
e r
sul piano (cioè con le coordinate xey), si può calcolare il segno della seguente determinante
Se il determinante è negativo (cioè Orient(p, q, r) < 0
), il poligono è orientato in senso orario (CW). Se il determinante è positivo (cioè Orient(p, q, r) > 0
), il poligono è orientato in senso antiorario (CCW). Il determinante è zero (cioè Orient(p, q, r) == 0
) se punti p
, q
e r
sono collineari .
Nella formula precedente, abbiamo anteporre quelli davanti alle coordinate p
, q
e r
perché stiamo usando coordinate omogenee .
Penso che per assegnare alcuni punti in senso orario tutti i bordi debbano essere positivi non solo la somma dei bordi. Se un bordo è negativo di almeno 3 punti vengono dati in senso antiorario.
La mia soluzione C # / LINQ si basa sui consigli sui prodotti incrociati di @charlesbretana di seguito. È possibile specificare un riferimento normale per l'avvolgimento. Dovrebbe funzionare fintanto che la curva si trova principalmente nel piano definito dal vettore in alto.
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;
}
}
}
con un test unitario
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();
}
}
}
Questa è la mia soluzione usando le spiegazioni nelle altre risposte:
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
Un metodo molto più semplice dal punto di vista computazionale, se conosci già un punto all'interno del poligono :
Scegli qualsiasi segmento di linea dal poligono originale, i punti e le loro coordinate in quell'ordine.
Aggiungi un punto "interno" noto e forma un triangolo.
Calcola CW o CCW come suggerito qui con questi tre punti.
Dopo aver testato diverse implementazioni inaffidabili, l'algoritmo che ha fornito risultati soddisfacenti per quanto riguarda l'orientamento CW / CCW è stato quello pubblicato da OP in questo thread ( shoelace_formula_3
).
Come sempre, un numero positivo rappresenta un orientamento in senso antiorario, mentre un numero negativo in senso antiorario.
Ecco la soluzione rapida 3.0 basata sulle risposte sopra:
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
Un'altra soluzione per questo;
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;
}
Prendi tutti i vertici come un array come questo;
const vertices = [{x: 5, y: 0}, {x: 6, y: 4}, {x: 4, y: 5}, {x: 1, y: 5}, {x: 1, y: 0}];
isClockwise(vertices);
Soluzione per R per determinare la direzione e invertire se in senso orario (trovato necessario per gli oggetti 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
Sebbene queste risposte siano corrette, sono matematicamente più intense del necessario. Assumi le coordinate della mappa, dove il punto più a nord è il punto più alto sulla mappa. Trova il punto più a nord, e se 2 punti pareggiano, è il più a nord, quindi il più a est (questo è il punto che usa la risposta nella sua risposta). Nei tuoi punti,
punto [0] = (5,0)
punto [1] = (6,4)
punto [2] = (4,5)
punto [3] = (1,5)
punto [4] = (1,0)
Se assumiamo che P2 sia il punto più a nord, allora il punto precedente o successivo determinano in senso orario, CW o CCW. Poiché il punto più a nord si trova sulla parete nord, se P1 (precedente) a P2 si sposta verso est, la direzione è CW. In questo caso, si sposta verso ovest, quindi la direzione è in senso antiorario come dice la risposta accettata. Se il punto precedente non ha movimento orizzontale, lo stesso sistema si applica al punto successivo, P3. Se P3 è a ovest di P2, lo è, allora il movimento è in senso antiorario. Se il movimento da P2 a P3 è est, in questo caso è ovest, il movimento è CW. Supponiamo che nte, P2 nei dati, sia il punto più a nord rispetto a est e che il prv sia il punto precedente, P1 nei dati e nxt sia il punto successivo, P3 nei dati e [0] sia orizzontale o est / ovest dove ovest è minore di est e [1] è verticale.
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)
.x
e .y
di una struttura, invece di [0]
e [1]
. Non sapevo cosa dicesse il tuo codice, la prima volta che l'ho guardato.)
Codice C # per implementare la risposta di 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;
}
Ecco una semplice implementazione di Python 3 basata su questa risposta (che, a sua volta, si basa sulla soluzione proposta nella risposta accettata )
def is_clockwise(points):
# points is your list (or array) of 2d points.
assert len(points) > 0
s = 0.0
for p1, p2 in zip(points, points[1:] + [points[0]]):
s += (p2[0] - p1[0]) * (p2[1] + p1[1])
return s > 0.0
trova il centro di massa di questi punti.
supponiamo che ci siano linee da questo punto ai tuoi punti.
trova l'angolo tra due linee per line0 line1
di farlo per line1 e line2
...
...
se questo angolo aumenta monotonicamente di quanto non sia in senso antiorario,
altrimenti se diminuendolo monotonicamente è in senso orario
altro (non è monotonico)
non puoi decidere, quindi non è saggio