web-dev-qa-db-ja.com

ポリゴンポイントのリストが時計回りの順序になっているかどうかを確認する方法は?

ポイントのリストがある場合、それらが時計回りの順序であるかどうかを確認するにはどうすればよいですか?

例えば:

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

反時計回り(または一部の人にとっては反時計回り)と言うでしょう。

232
Stécy

三日月形などの非凸多角形の場合、推奨される方法の一部は失敗します。以下は、非凸ポリゴンで動作するシンプルなものです(8のような自己交差ポリゴンでも動作し、 主に 時計回り)。

エッジの合計(x2 − x1)(y2 + y1)。結果が正の場合、曲線は時計回りになり、負の場合、曲線は反時計回りになります。 (結果は、+ /-規則で囲まれた領域の2倍になります。)

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

外積 は、2つのベクトルの垂直度を測定します。ポリゴンの各Edgeが3次元(3-D)xyz空間のx-y平面内のベクトルであると想像してください。次に、2つの連続したエッジの外積は、z方向のベクトルです(2番目のセグメントが時計回りの場合は正のz方向、反時計回りの場合はマイナスのz方向)。このベクトルの大きさは、2つの元のエッジ間の角度のサインに比例するため、垂直の場合は最大になり、エッジが同一線上(平行)の場合は消えて消えます。

したがって、ポリゴンの各頂点(ポイント)について、隣接する2つのエッジの外積の大きさを計算します。

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までのセグメントです。
edgeBpoint1からpoint2の間
...
edgeEpoint4point0の間です。

次に、頂点A(point0)は
edgeE [point4からpoint0へ]
edgeA [point0から `point1 'まで

これらの2つのエッジ自体はベクトルであり、そのx座標とy座標は、開始点と終了点の座標を減算することで決定できます。

edgeE = point0-point4 = (1, 0) - (5, 0) = (-4, 0)および
edgeA = point1-point0 = (6, 4) - (1, 0) = (5, 4)および

そして、これらの2つの隣接するエッジの外積は、次の行列の行列式を使用して計算されます。この行列は、3つの座標軸(ij、&k)を表すシンボルの下に2つのベクトルの座標を配置することによって構築されます。 3番目の(ゼロ)値の座標は、クロス積の概念が3次元構造であるために存在します。したがって、これらの2次元ベクトルを3次元に拡張して、クロス積を適用します。

 i    j    k 
-4    0    0
 1    4    0    

すべての外積が乗算される2つのベクトルの平面に垂直なベクトルを生成することを考えると、上記の行列の行列式はk(またはz軸)コンポーネントのみを持ちます。
kまたはz軸成分の大きさを計算する式は
a1*b2 - a2*b1 = -4* 4 - 0* 1 = -16

この値の大きさ(-16)は、2つの元のベクトル間の角度のサインの尺度であり、2つのベクトルの大きさの積で乗算されます。
実際には、その値の別の式は
A X B (Cross Product) = |A| * |B| * sin(AB)

したがって、角度の測定値に戻るには、この値(-16)を2つのベクトルの大きさの積で除算する必要があります。

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

したがって、sin(AB)= -16 / 16.4924 = -.97014...の尺度

これは、頂点の後の次のセグメントが左に曲がったのか右に曲がったのか、そしてどれだけの尺度かです。アークサインを取る必要はありません。気にするのは、その大きさ、そしてもちろんその兆候(正または負)だけです!

閉じたパスの周囲にある他の4つのポイントごとにこれを行い、各頂点でこの計算の値を合計します。

最終合計が正の場合、時計回り、負、反時計回りに行きました。

49
Charles Bretana

これはかなり古い質問だと思いますが、とにかく別の解決策を捨てるつもりです。なぜなら、それは単純で数学的に集中的ではないからです-基本的な代数を使うだけです。ポリゴンの符号付き面積を計算します。負の場合、ポイントは時計回りの順序になり、正の場合、ポイントは反時計回りになります。 (これはベータ版のソリューションに非常に似ています。)

符号付き面積を計算します:A = 1/2 *(x1* y2 - バツ2* y1 + x2* y3 - バツ3* y2 + ... + xn* y1 - バツ1* yn

または擬似コードで:

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

43
Sean the Bean

最小のy(およびタイがある場合は最大のx)を持つ頂点を見つけます。頂点をAとし、リスト内の前の頂点をBとし、リスト内の次の頂点をCとします。ここで、ABACの外積のsignを計算します。


参照:

25
lhf

this answer に基づくアルゴリズムの簡単なC#実装を次に示します。

タイプVectorXおよび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;
}

%は、モジュロ演算を実行するモジュロまたは剰余演算子です( Wikipediaによる )。ある数値を別の数値で除算した後、剰余を見つけます。

頂点の1つから開始し、各辺の範囲を定める角度を計算します。

最初と最後はゼロになります(それらをスキップします)。残りについては、角度のサインは、(point [n] -point [0])と(point [n-1] -point [0])の単位長さに対する正規化の外積によって与えられます。

値の合計が正の場合、ポリゴンは反時計回りに描画されます。

6
Steve Gilham

JavaScriptでの Seanの答え の実装:

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

これが正しいと確信しています。それは働いているようです:-)

これらのポリゴンは、次のように見えます:

4
mpen

価値のあるものとして、私はこのミックスインを使用して、Google Maps API v3アプリの巻き上げ順序を計算しました。

コードはポリゴン領域の副作用を活用します。頂点の時計回りの巻き順は正の領域を生成し、同じ頂点の反時計回りの巻き順は負の値と同じ領域を生成します。このコードは、Google Mapsジオメトリライブラリの一種のプライベートAPIも使用します。私はそれを使用して快適に感じました-あなた自身の責任で使用してください。

サンプル使用法:

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;
  }
})();
4
Steve Jansen

これは、 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);
}
2
MSS

Matlabを使用する場合、ポリゴンの頂点が時計回りの順序にある​​場合、関数 ispolycw はtrueを返します。

2
Frederick

このウィキペディアの記事でも説明されているように、 曲線の向き 、平面上のpqrの3点が与えられると(つまり、xおよびy座標で)、次の行列式の符号を計算できます

enter image description here

行列式が負の場合(つまりOrient(p, q, r) < 0)、ポリゴンは時計回り(CW)に向けられます。行列式が正の場合(つまり、Orient(p, q, r) > 0)、ポリゴンは反時計回り(CCW)に向けられます。ポイントpq、およびr共線性 である場合、行列式はゼロ(つまりOrient(p, q, r) == 0)です。

上記の式では、 同次座標 を使用しているため、pq、およびrの座標の前に1つを追加します。

1
Ian

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
0
dez

一部のポイントを時計回りに与えるには、エッジの合計だけでなく、すべてのエッジが正である必要があると思います。 1つのエッジが負の場合、少なくとも3つのポイントが反時計回りに与えられます。

0
daniel

上記の回答に基づいたSwift 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
0
Toby Evetts

このための別のソリューション。

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);
0
Ind

これは、他の回答の説明を使用した私のソリューションです:

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

これらの答えは正しいですが、必要以上に数学的に強烈です。最北点が地図上の最高点である地図座標を想定します。最も北のポイントを見つけ、2つのポイントが同点の場合、それが最も北で最も東になります(これはlhfが答えで使用するポイントです)。あなたのポイントでは、

point [0] =(5,0)

point [1] =(6,4)

ポイント[2] =(4,5)

point [3] =(1,5)

ポイント[4] =(1,0)

P2が最も北であると仮定した場合、東のポイントは前または次のポイントのいずれかで時計回り、CW、またはCCWを決定します。最も北のポイントは北の面にあるため、P2へのP1(前の)が東に移動すると、方向はCWになります。この場合、それは西に移動するので、受け入れられた答えが示すように方向はCCWです。前のポイントに水平方向の動きがない場合、同じシステムが次のポイントP3に適用されます。 P3がP2の西にある場合、PWはCCWです。 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'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

いくつかの信頼性の低い実装をテストした後、箱から出したCW/CCWの向きに関する満足のいく結果を提供したアルゴリズムは、OP in this thread(shoelace_formula_3)によって投稿されたものでした。

いつものように、正の数はCW方向を表し、負の数はCCWを表します。

0
Marjan Moderc

実装するC#コード lhf's answer

// 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

計算がはるかに簡単な方法、ポリゴン内のポイントを既に知っている場合

  1. 元のポリゴン、ポイント、およびその座標から任意のラインセグメントをこの順序で選択します。

  2. 既知の「内部」ポイントを追加し、三角形を形成します。

  3. 推奨されるようにCWまたはCCWを計算します ここ これらの3つのポイントで。

0
Venkata Goli

私のC#/ 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;

        } 

    }
}

ユニットテスト付き

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