有一组点的列表,如何判断它们是否按顺时针排序?
例如:
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)
我会说这是逆时针方向(或者一些人称之为顺时针)。
一些建议的方法在非凸多边形(例如新月形)的情况下可能失败。这里有一个简单的方法,可以处理非凸多边形(甚至可以处理像8字形这样的自相交多边形),并告诉你它是否是顺时针方向。
对边进行求和,(x2 − x1)(y2 + y1)。如果结果为正,则曲线为顺时针;如果结果为负,则曲线为逆时针。(结果是封闭面积的两倍,具有 +/- 约定。)
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]))
。即,必须使用模运算将索引取模为N(N≡0
)。该公式仅适用于闭合多边形,多边形没有虚构的边缘。 - Olivier Jacot-Descombes找到y值最小(如果有并列则选择x值最大)的顶点。让这个顶点为A
,在列表中前一个顶点是B
,后一个顶点是C
。现在计算AB
和AC
的向量积的符号。
参考文献:
常问问题:comp.graphics.algorithms中的如何确定简单多边形的方向?
维基百科上的曲线方向。
O(1)
的解决方案。所有其他答案都为O(n)
,其中n
是多边形点的数量。要进行更深层次的优化,请参见维基百科的实用考虑子节,该子节是关于曲线方向的精彩文章。 - Cecil CurryO(1)
:**(A)** 多边形为凸多边形(在这种情况下,任意一个顶点都位于凸包上,因此足以满足要求)或 (B) 您已经知道具有最小Y坐标的顶点。如果不符合上述情况(即多边形是非凸多边形且您对其一无所知),则需要进行O(n)
搜索。然而,由于不需要求和,因此与其他简单多边形的解决方案相比,这仍然快得多。 - Cecil Curry我提供另一种解决方案,它比较直接,不需要进行复杂的数学计算,只需要使用基本的代数知识。 首先计算多边形的有向面积。如果它是负数,则点按顺时针顺序排列;如果它是正数,则点按逆时针顺序排列。这和 Beta 的解决方案非常相似。
计算多边形的有向面积:
A = 1/2 * (x1*y2 - x2*y1 + x2*y3 - x3*y2 + ... + xn*y1 - x1*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。
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
)的符号下面的两个向量的坐标放置在一起。第三个(值为零)的坐标存在是因为叉积概念是一个三维构造,因此我们将这些二维向量扩展到三维以应用叉积:
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 (叉积) = |A| * |B| * sin(AB)
因此,如果只需要得到角度的测量结果,则需要将该值 (-16
) 除以两个向量长度的乘积。
|A| * |B|
= 4 * Sqrt(17)
= 16.4924...
所以,sin(AB) 的值为 -16 / 16.4924
= -.97014...
这是下一个顶点后线段向左或向右弯曲程度的测量值。不需要使用反正弦函数。我们关心的只是它的大小和符号(正或负)!这是一个基于@Beta的答案的简单C#算法实现。
假设我们有一个Vector
类型,具有类型为double
的X
和Y
属性。
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;
}
%
是求模或取余运算符,执行该运算可以在一个数除以另一个数后找到余数,具体信息请参考维基百科的相关页面。
根据 @MichelRouzic 的评论所述,这是一个经过优化的版本:
double sum = 0.0;
Vector v1 = vertices[vertices.Count - 1]; // or vertices[^1] with
// C# 8.0+ and .NET Core
for (int i = 0; i < vertices.Count; i++) {
Vector v2 = vertices[i];
sum += (v2.X - v1.X) * (v2.Y + v1.Y);
v1 = v2;
}
return sum > 0.0;
这不仅节省了取模运算符%
,还节省了数组索引。
测试(请参阅与 @ WDUK 的讨论)
public static bool IsClockwise(IList<(double X, double Y)> vertices)
{
double sum = 0.0;
var v1 = vertices[^1];
for (int i = 0; i < vertices.Count; i++) {
var v2 = vertices[i];
sum += (v2.X - v1.X) * (v2.Y + v1.Y);
Console.WriteLine($"(({v2.X,2}) - ({v1.X,2})) * (({v2.Y,2}) + ({v1.Y,2})) = {(v2.X - v1.X) * (v2.Y + v1.Y)}");
v1 = v2;
}
Console.WriteLine(sum);
return sum > 0.0;
}
public static void Test()
{
Console.WriteLine(IsClockwise(new[] { (-5.0, -5.0), (-5.0, 5.0), (5.0, 5.0), (5.0, -5.0) }));
// infinity Symbol
//Console.WriteLine(IsClockwise(new[] { (-5.0, -5.0), (-5.0, 5.0), (5.0, -5.0), (5.0, 5.0) }));
}
(-5 - 5) * (-5 + -5) = 100
?The first two points are (-5,-5) and (-5,5)So v2.x - v1.x will always be 0 so how did you get 100 for the first two points?((-5) - (-5)) * ((-5) - 5) = 0
- WDUKv1 = vertices [^ 1]
)和第一个(v2
)点计算的。因此,您提到的零作为测试中的第二行打印出来。 - Olivier Jacot-Descombes以下是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));
我很确定这是正确的。它似乎正在工作 :-)
如果你想知道,这些多边形看起来像这样:
从其中一个顶点开始,计算每条边对应的角度。
第一条边和最后一条边的角度为零(因此跳过它们);对于其余的边,正弦值由将(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;
}
})();
这是OpenLayers 2实现的功能。拥有顺时针多边形的条件是面积 < 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);
}
以下是实现 lhf的答案 的C#代码:
// 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;
}