有一个点列表,如何找到它们是否按顺时针顺序排列?
例如:
point[0] = (5,0)
point[1] = (6,4)
point[2] = (4,5)
point[3] = (1,5)
point[4] = (1,0)
会说它是逆时针(对于某些人来说是逆时针)。
有一个点列表,如何找到它们是否按顺时针顺序排列?
例如:
point[0] = (5,0)
point[1] = (6,4)
point[2] = (4,5)
point[3] = (1,5)
point[4] = (1,0)
会说它是逆时针(对于某些人来说是逆时针)。
Answers:
在非凸多边形(例如新月形)的情况下,某些建议的方法将失败。这是一个适用于非凸多边形的简单示例(它甚至可以与自相交的多边形(例如,八字形图形)一起使用,告诉您它是否大部分是顺时针方向)。
边的总和为(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
Sum( (x[(i+1) mod N] - x[i]) * (y[i] + y[(i+1) mod N]) )
i = 0到N-1。即,必须必须使用指数Modulo N(N ≡ 0
)该公式仅适用于封闭的多边形。多边形没有虚边。
所述叉积测量两个向量的垂直岬的程度。想象一下,多边形的每个边都是三维(3-D)xyz空间的xy平面中的向量。然后,两个连续边的叉积是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
[从 point0
到'point1'
这两个边本身就是向量,可以通过减去其起点和终点的坐标来确定其x和y坐标:
edgeE
= point0
- point4
= (1, 0) - (5, 0)
= (-4, 0)
和
edgeA
= point1
- point0
= (6, 4) - (1, 0)
= (5, 4)
和
并且使用下面的矩阵,这是通过将代表这三个符号下方的两个向量的坐标的坐标轴构成的行列式计算这两个邻接边缘的叉积(i
,j
,& k
)。因为叉积概念是3-D构造,所以存在第三个(零)值坐标,因此我们将这些2-D向量扩展为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
)的大小是两个原始矢量之间角度正弦的量度,乘以两个矢量的大小乘积。
实际上,其值的另一个公式是
A X B (Cross Product) = |A| * |B| * sin(AB)
。
因此,要返回到仅一个角度的度量,您需要将该值(-16
)除以两个矢量的大小的乘积。
|A| * |B|
= 4 * Sqrt(17)
=16.4924...
所以sin(AB)的度量= -16 / 16.4924
=-.97014...
这是对顶点之后的下一个段是否向左或向右弯曲以及弯曲多少的度量。无需采取反正弦曲线。我们只关心它的大小,当然还有它的符号(正或负)!
对闭合路径周围的其他4个点分别执行此操作,然后将每个顶点上此计算中的值相加。
如果最终总和为正,则顺时针为负,逆时针为负。
我想这是一个很老的问题,但是无论如何我都会抛出另一个解决方案,因为它很简单,而且数学上并不密集-它只使用基本代数。计算多边形的有符号区域。如果为负,则点按顺时针方向排列;如果为正,则点按逆时针方向排列。(这与Beta的解决方案非常相似。)
计算有符号区域: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。
previousPoint
为下一次迭代。在开始循环之前,将其设置previousPoint
为数组的最后一点。需要权衡的是额外的局部变量副本,但较少的数组访问。最重要的是,不必触摸输入数组。
找到具有最小y的顶点(如果有联系,则找到最大的x)。令顶点为A
,列表中B
的上一个顶点为,列表中的下一个顶点为C
。现在计算符号的叉积的AB
和AC
。
参考文献:
如何找到简单多边形的方向?在 常见问题解答中:comp.graphics.algorithms。
维基百科上的曲线方向。
O(1)
解决方案。所有其他答案可得出多边形点数量的O(n)
解n
。要获得更深入的优化,请参阅Wikipedia出色的“ 曲线方向”文章的“ 实用注意事项”小节。
O(1)
仅当以下情况之一时,该解决方案:(A)此多边形是凸的(在这种情况下,任意顶点位于凸包上,因此就足够了)或 (B)您已经知道Y坐标最小的顶点。如果不是这种情况(即,该多边形是非凸的,并且您对此一无所知),O(n)
则需要进行搜索。但是,由于不需要求和,因此它仍然比简单多边形的任何其他解决方案都快得多。
这是基于此答案的算法的简单C#实现。
假设我们有一个Vector
类型为X
且Y
属性为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];
sum += (v2.X - v1.X) * (v2.Y + v1.Y);
}
return sum > 0.0;
}
%
是执行求模运算的模或余数运算符(根据Wikipedia)在将一个数除以另一个后找到余数。
从其中一个顶点开始,然后计算每侧对向的角度。
第一个和最后一个将为零(因此跳过那些);对于其余部分,角度的正弦将由归一化为(point [n] -point [0])和(point [n-1] -point [0])的单位长度的叉积给出。
如果值的总和为正,则将以逆时针方向绘制多边形。
物有所值,我使用了这个mixin来计算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 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;
}
})();
一个实现 肖恩的回答在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));
可以肯定,这是正确的。它似乎正在工作:-)
如果您想知道,这些多边形看起来像这样:
这是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);
}
正如在此Wikipedia文章“ 曲线方向”中给出的,给定3个点p
,q
并且r
在平面上(即具有x和y坐标),您可以计算以下行列式的符号
如果行列式为负(即Orient(p, q, r) < 0
),则多边形的方向为顺时针(CW)。如果行列式为正(即Orient(p, q, r) > 0
),则多边形的方向为逆时针(CCW)。行列式为零(即Orient(p, q, r) == 0
),如果点是p
,q
并且r
是共线的。
在上面的公式中,我们在预先考虑的坐标的前的那些p
,q
和r
因为我们使用齐次坐标。
我认为,为了使某些点沿顺时针方向给出,不仅所有边之和,所有边都必须为正。如果一个边为负,则至少有3个点是逆时针方向给出的。
我的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();
}
}
}
这是我使用其他答案中的解释的解决方案:
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
如果您已经知道多边形内的一个点,则这是一种计算简单得多的方法:
从原始多边形,点及其坐标中依次选择任何线段。
添加一个已知的“内部”点,并形成一个三角形。
按照这三点计算此处建议的CW或CCW 。
在测试了几种不可靠的实现之后,提供了关于CW / CCW定向的开箱即用的令人满意结果的算法是由OP在此线程(shoelace_formula_3
)中发布的。
与往常一样,正数表示CW方向,而负数表示CCW。
这是基于上述答案的快速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
另一个解决方案;
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);
R确定方向并逆时针旋转的解决方案(发现单向物体是必需的):
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
尽管这些答案是正确的,但它们在数学上比必要的更为激烈。假设地图坐标,其中最北点是地图上的最高点。找到最北的点,如果有2个点平局,那就是最北,然后是最东(这是lhf在他的答案中使用的点)。在您看来
点[0] =(5,0)
点[1] =(6,4)
点[2] =(4,5)
点[3] =(1,5)
点[4] =(1,0)
如果我们假定P2是最北的,则上一个点或下一个点的东点确定为顺时针,顺时针或逆时针。由于最北点在北面上,因此如果P1(先前的P2)向东移动,则方向为CW。在这种情况下,它向西移动,因此,如接受的答案所示,方向为CCW。如果前一点没有水平运动,则相同的系统适用于下一点P3。如果P3在P2的西边,则为CCW。如果P2到P3的运动是东方的,那么在这种情况下,它的运动就是西方的。假设数据中的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)
.x
和.y
一个结构的,而不是[0]
和[1]
我不知道你的代码是说,我第一次在它一眼。)
C#代码实现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;
}
这是一个基于此答案的简单Python 3实现(相应地,该实现基于已接受答案中提出的解决方案)
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
找到这些点的质心。
假设从此点到您的点有直线。
查找line0 line1的两条线之间的角度
比第1行和第2行
...
...
如果此角度单调递增,而不是逆时针递增,
否则,如果单调递减,则顺时针
其他(不是单调的)
你不能决定,所以这是不明智的