原文地址: https://sean.cm/a/polygon-clipping-pt1

Greiner-Hormann裁剪算法无法处理重合线。 所以我研究并写了另一篇适用于所有多边形的文章。

在此处阅读后续内容:多边形裁剪(第 2 部分)

问题

首先, 让我们定义问题,

假设您有两个多边形,每个多边形都以 2D 形式存在

var poly1 = [ // red[ 181, 270 ],[  85, 418 ],[ 171, 477 ],[ 491, 365 ],[ 218, 381 ],[ 458, 260 ]
];
var poly2 = [ // blue[ 474, 488 ],[ 659, 363 ],[ 255, 283 ],[  56, 340 ],[ 284, 488 ],[ 371, 342 ]
];

奇偶规则

多边形遵循奇偶规则来确定一个点是否被视为区域“内部”。

基本规则是想象您正在用一条水平线上从左到右扫描。每次越过边缘时,都会在外部和内部之间切换。

那么:给定这两个多边形,我们如何计算不同的布尔运算?

基本理念

首先,让我们定义一些基本的规则.

顺时针vs逆时针(Forward vs. Backward Movement)

如果我们坐在多边形的任何一点上,我们总是可以向前一个点或者后一个点移动

顺时针只是意味着沿箭头方向移动,逆时针则相反

插入点

在处理过程中,我们需要在多边形中插入点。只要我们对如何插入它们很聪明,它就不会改变多边形的形状:

交叉点

识别和分类交叉点是算法中的魔法.

如果您考虑一下,我们将执行的每个操作(交集、联合、差异)都会产生一个包含多边形之间所有交点的多边形

我们不关心同一多边形内的交叉点.

另外:如果你想象我们正沿着一个多边形行走并遇到一个十字路口,我们有3个选择.

1. 保持在同一个多边形上(这是毫无意义的)

2. 切换多边形,开始顺时针移动

3. 切换多边形,并开始逆时针移动

因此,如果我们能够智能地选择在每个交叉路口的方向,我们就可以追踪到正确的结果形状.

交叉点示例

想象一下, 我们想象一下,我们正在追踪两个多边形合并union的结果.

在每个交叉点,我们都希望朝着最终形状继续增长的方向移动.

我们可以这样做:

我们也可以在相反的方向得到相同的结果:

关于所有四个决定,我们可以说哪些是正确的?

在每个交叉点,我们总是朝着远离我们离开的多边形的方向前进.

因此,例如,如果我们沿着 Blue 行驶,然后遇到一个十字路口,我们应该继续沿着 Red 向远离Blue的方向行使.

会有什么不同?这是Red-Blue(从Red中减去Blue区域):

而在另一个方向:

对此我们能说什么?

当从红色切换到蓝色时,我们进入红色。当从蓝色切换到红色时,我们远离红色.

所以我们有两个基本的决定:

1. 当从红色切换到蓝色时,我们是进入还是远离红色?

2.当从蓝色切换到红色时,我们是进入还是远离蓝色?

对于联合(union)来说, 答案总是离开. 但是对于Red-Blue(Red减Blue),我们想要进入红色, 远离蓝色。如果你玩玩,你会注意到交叉(intersection )意味着总是进入你要离开的

这给了我们下边的表

Operator Into Red? Into Blue?
联合(Union) False False
Red减Blue(Red - Blue) True False
Blue减Red(Blue - Red) False True
交叉(Intersection) True True

交叉入口/ 交叉出口

我们不知道如何进入离开——我们只知道沿着多边形顺时针,逆时针移动。我们如何把两者同意起来.

如果我们在一个交点的两边取两个点,并测试它们是否在另一个多边形内,我们可以保证一个点在外面,一个点在里面:

如果第一个点在外面,那么我们可以认为这条线是通过交点进入多边形的。如果第一个点在,则该线通过交点离开多边形.

所以,我们真的只需要将每个交叉点标记为交叉入口/ 交叉出口

当我们沿着一条路径行驶时,每个路口都会切换我们是在里面还是外面。它必须.

因此,我们只需要计算第一个点是否在另一个多边形内部。如果是,那么第一个交叉点是一个交叉出口——否则第一个交叉点是一个交叉入口.

而且由于路径上的每个交叉点都在entryexit之间切换,我们不必继续测试点是在内部还是外部(这很昂贵).

表现

最后,重要的是要认识到交叉点是相对于多边形的交叉入口或交叉出口.

这意味着每个交叉点有四种可能性.

白色代表进入,黑色代表退出。左半球为红色,右半球为蓝色

实际上,对于每个交点,我们将在每个多边形中插入一个点。所以每个交点会有两个点,一个存储在每个多边形中。每个点都会跟踪它是进入还是退出.

现在我们准备好代码啦.

步骤1. 将多边形转换为链表

双链表对于这个算法来说是一个有用的多边形表示,因为我们将同时插入点和遍历。通过使用双链表,我们不必担心插入会破坏遍历.

我们还需要跟踪一个点是否是一个交点,所以我们可以从false这里初始化它开始:

function UpgradePolygon(p){// converts a list of points into a double linked listvar root = null;for (var i = 0; i < p.length; i++){var node = {point: p[i],intersection: false,next: null,prev: null};if (root === null){// root just points to itself://    +-> (root) <-+//    |            |//    +------------+node.next = node;node.prev = node;root = node;}else{// change this://    ...-- (prev) <--------------> (root) --...// to this://    ...-- (prev) <--> (node) <--> (root) --...var prev = root.prev;prev.next = node;node.prev = prev;node.next = root;root.prev = node;}}return root;
}

步骤2. 计算并插入交叉点

接下来,我们需要遍历每个边组合,看看它们是否相交。如果它们确实彼此相交,那么我们需要在多边形中插入交点.

线交点

首先,我们需要一个辅助函数来计算两条线的交点:

function LinesIntersect(a0, a1, b0, b1){var adx = a1[0] - a0[0];var ady = a1[1] - a0[1];var bdx = b1[0] - b0[0];var bdy = b1[1] - b0[1];var axb = adx * bdy - ady * bdx;var ret = {cross: axb,alongA: Infinity,alongB: Infinity,point: [Infinity, Infinity]};if (axb === 0)return ret;var dx = a0[0] - b0[0];var dy = a0[1] - b0[1];ret.alongA = (bdx * dy - bdy * dx) / axb;ret.alongB = (adx * dy - ady * dx) / axb;ret.point = [a0[0] + ret.alongA * adx,a0[1] + ret.alongA * ady];return ret;
}

它计算两条线的交点,并返回每条线上的交点“沿”多远。因此,例如,如果alongA0.75,那么交集发生在从a0到 的75% 处a1.

这些值是重要的,因为他们可能是负数或大于1,因此,如果两条线实际相交,我们需要测试alongAalongB0和1(不含)之间.

下一个非交点

由于我们将在我们的链表中插入交点,所以有一个帮助函数来查找下一个交点.

function NextNonIntersection(node){do{node = node.next;} while (node.intersection);return node;
}

每个边组合(Edge Pair)

现在我们可以编写迭代每个边组合的代码:

var root1 = UpgradePolygon(poly1);
var root2 = UpgradePolygon(poly2);var here1 = root1;
var here2 = root2;
do{do{//// TODO: test intersection between://    here1 -> NextNonIntersection(here1)  and//    here2 -> NextNonIntersection(here2)//here2 = NextNonIntersection(here2);} while (here2 !== root2);here1 = NextNonIntersection(here1);
} while (here1 !== root1);

交叉点测试

给定两个节点,我们可以测试交集:

var next1 = NextNonIntersection(here1);
var next2 = NextNonIntersection(here2);var i = LinesIntersect(here1.point, next1.point,here2.point, next2.point
);if (i.alongA > 0 && i.alongA < 1 &&i.alongB > 0 && i.alongB < 1){//// TODO: insert intersection points in both polygons at//       the correct location, referencing each other//
}

插入交叉点

最后,如果两条边相交,那么我们要在两个非交点之间插入我们的交叉点.

为了将它插入正确的位置,我们必须跟踪alongAalongB值以确保如果两个交点在同一条边上,它们以正确的顺序插入.

我们将要创建两个节点,一个用于每个多边形——但这些节点应该相互指向,以便我们稍后在遇到交叉点时可以在多边形之间“跳跃”

var node1 = {point: i.point,intersection: true,next: null,prev: null,dist: i.alongA,friend: null
};
var node2 = {point: i.point,intersection: true,next: null,prev: null,dist: i.alongB,friend: null
};// point the nodes at each other
node1.friend = node2;
node2.friend = node1;var inext, iprev;// find insertion between here1 and next1, based on dist
inext = here1.next;
while (inext !== next1 && inext.dist < node1.dist)inext = inext.next;
iprev = inext.prev;// insert node1 between iprev and inext
inext.prev = node1;
node1.next = inext;
node1.prev = iprev;
iprev.next = node1;// find insertion between here2 and next2, based on dist
inext = here2.next;
while (inext !== next2 && inext.dist < node2.dist)inext = inext.next;
iprev = inext.prev;// insert node2 between iprev and inext
inext.prev = node2;
node2.next = inext;
node2.prev = iprev;
iprev.next = node2;

步骤3. 计算交叉入口/交叉出口

我们知道交叉口在进入和退出之间交替。但是第一个交叉点是什么?是入口还是出口.

简单:如果多边形的第一个点在另一个多边形内,那么第一个交点必须是出口.

但是,计算一个点是否在多边形内部实际上有点复杂.

点在多边形内

function PointInPolygon(point, root){var odd = false;var x = point[0];var y = point[1];var here = root;do {var next = here.next;var hx = here.point[0];var hy = here.point[1];var nx = next.point[0];var ny = next.point[1];if (((hy < y && ny >= y) || (hy >= y && ny < y)) &&(hx <= x || nx <= x) &&(hx + (y - hy) / (ny - hy) * (nx - hx) < x)){odd = !odd;}here = next;} while (here !== root);return odd;
}

PointInPolygon通过计算水平线相交的边数来工作。水平线从(-Infinity, y)(x, y)。它只关心交叉点的数量是奇数还是偶数。它基于光线投射。

交替进入/退出

现在我们可以轻松计算出一个交叉点是入口还是出口:

function CalculateEntryExit(root, isEntry){var here = root;do{if (here.intersection){here.isEntry = isEntry;isEntry = !isEntry;}here = here.next;} while (here !== root);
}var is1in2 = PointInPolygon(root1.point, root2);
var is2in1 = PointInPolygon(root2.point, root1);CalculateEntryExit(root1, !is1in2);
CalculateEntryExit(root2, !is2in1);

步骤4. 生成结果

我们已经走了很长一段路!这是我们到目前为止所拥有的.

我们已经计算并插入了交点,并将它们标记为每个多边形的入口或出口.

现在是有趣的部分!

从哪里开始

我们从哪里开始追踪结果?我们不能只选择一个随机点,因为有些点实际上可以从结果中完全删除.

由于所有操作都包括每个交集,我们应该从寻找未处理的交集开始.

我们添加到最终结果中的每个交点,我们都标记为已处理.

然后,我们只是继续跟踪,直到我们不再有任何交集需要处理.

var result = [];
var isect = root1;
var into = [intoBlue, intoRed]; // explained below
while (true){do{if (isect.intersection && !isect.processed)break;isect = isect.next;} while (isect !== root1);if (isect === root1)break;//// TODO: process isect//
}

转向哪个方向

最后,我们来到了症结所在:

当我们遇到十字路口时,我们怎么知道该往哪个方向转弯?

让我们来推理一下:

Is Entry? Move Into? Move Forward?
True True True
True False False
False True False
False False True

因此,如果 ,我们应该继续前进isEntry === intoPoly

由于我们所在的多边形来回切换,我们只需通过将intoBlue和存储intoRedinto列表中来使我们的决策动态化,并将 其curpoly用作索引.

var curpoly = 0;
var clipped = [];var here = isect;
do{// mark intersection as processedhere.processed = true;here.friend.processed = true;var moveForward = here.isEntry === into[curpoly];do{clipped.push(here.point);if (moveForward)here = here.next;elsehere = here.prev;} while (!here.intersection);// we've hit the next intersection so switch polygonshere = here.friend;curpoly = 1 - curpoly;
} while (!here.processed);result.push(clipped);

没有交叉点

如果没有交叉点?

我们的结果集将是空的……这可能是正确的,也可能是错误的——这取决于操作.

一个简单的检查就足以修复它:

if (result.length <= 0){if (is1in2 === intoBlue)result.push(poly1);if (is2in1 === intoRed)result.push(poly2);
}

演示

单击此处启动演示!

您可以拖动每个多边形的点,并通过单击按钮切换操作。

附录:限制

抱歉,这个算法有一个严重的局限性:

您不能拥有完美重叠的点或边.

如果你仔细想想,这是有道理的:整个算法都是基于交叉点的思想.

如果点或边直接重叠,那么您就不会得到那种好的跳跃效果.

最初的论文建议稍微“扰乱”点,这样线条就不会完全重叠。我最初认为这是一个小调整,不会有有问题.

但是,我错了.

扰动点会破坏数据——因此可能很重要的源数据的属性(例如,平滑边缘)变得无效.

幸运的是我研究了另一种处理一切的算法,并写了一篇后续文章

此处阅读后续内容:多边形裁剪(第 2 部分)

多边形裁剪(Polygon Clipping) 1相关推荐

  1. 多边形裁剪(Polygon Clipping) 2

    F. Martinez 2008算法处理重合边缘(不像格雷纳-霍曼),但它仍然有一些轻微的goofiness.让我们一劳永逸地解决这个问题. 重新来看下问题 给定两个多边形 我们如何计算不同的布尔运算 ...

  2. (Alan Murta)编制的多边形集合运算软件包(general polygon clipping library,简称GPC)

    今天无意中看到这个包,期待以久的好东西,发布的很早了,可惜我今天才看到,先存起来,下来慢慢看. (Alan Murta)编制的多边形集合运算软件包(general polygon clipping l ...

  3. SIwave仿真手册——软件基础(一)

    目录 一.软件基础 1.1 设计文档的导入 1.1.1 allegro文件的导入 1.1.2ODB++文件导入(建议选此,数据通用性较好) 1.2软件操作界面 1.2.1菜单栏 1.2.2 optio ...

  4. ClipperLib库使用说明

    前言 Clipper库是目前计算机图形届广为使用的图形处理库,可以用于解决平面二维图形的多边形简化.布尔运算和偏置处理,在CAD.加工路径与3D打印方面都有着比较重要的应用. 本文使用Love2.io ...

  5. Coding and Paper Letter(八十四)

    最近忙于研究事宜,许久未归.新一期资源整理博客. 1 Coding: 1.Python的Geohash编码压缩工具. georaptor 2.基于Google Earth Engine平台用于洪涝灾害 ...

  6. 重磅!谷歌刚刚发布Objectron新数据集,可完美检测3D目标,超过4百万幅图像和15K视频剪辑!...

    点击上方"3D视觉工坊",选择"星标" 干货第一时间送达 本文转载自:新智元  |  编辑:QJP [导读]谷歌人工智能实验室近日发布 Objectron 数据 ...

  7. 重磅!谷歌发布3D目标检测数据集及检测方案

    点上方蓝字计算机视觉联盟获取更多干货 在右上方 ··· 设为星标 ★,与你不见不散 仅作学术分享,不代表本公众号立场,侵权联系删除 转载于:新智元 AI博士笔记系列推荐 周志华<机器学习> ...

  8. 多边形交叉区域计算面积_用什么算法来找到多边形与圆之间的交叉区域?

    I have map. On the top of map layer have a polygon A and circle B. They are intersected each others. ...

  9. QGIS算法列表 (py console输出)

    要获取QGIS的算法列表,可以直接通过在QGIS提供的python console中取得. 点击中间python图标,打开QGIS自带的python console. 输入以下,打印出QGIS的算法列 ...

最新文章

  1. chrome jssip
  2. zedboard:使用ISE和modelsim搭建仿真环境
  3. 打开新窗口的js代码
  4. python packages_Python-Packages
  5. 应用负载均衡之LVS(三):ipvsadm命令
  6. linux 查看cpu
  7. 中科院合肥科学技术学校05计算机,硕士研究生课程设置-中国科学院合肥物质科学研究院.PDF...
  8. 现代软件工程 作业 1 个人项目
  9. html语言可以干什么,JavaScript语言能做什么?
  10. 与计算机交朋友优秀教案,《与计算机交朋友》教学设计-20210608120218.pdf-原创力文档...
  11. Office 2010、Project 2010、Visio 2010
  12. Ubuntu18.04下使用docker制作ubuntu20.04镜像
  13. getContext,getApplicationContext和this有什么区别
  14. 管理学大师彼得 德鲁克
  15. 爱希ISee人体感应器,雷达人体存在探测应用,毫米波雷达技术方案
  16. ICIP论文结构整理
  17. CSS 中哪些属性可以继承?
  18. Qt 给QWidget添加工具栏
  19. 计算机load代表,什么是 Load ? 什么是 Load Average ?
  20. R039---超越容易实现的目标,成功地扩展RPA的重要性

热门文章

  1. ABP框架—项目文件介绍(2)
  2. 2188 完成比赛的最少时间(递推)
  3. office连接oracle,office2013怎么连接32位oracle
  4. 网站服务器配置e5,从性能到配置 E5服务器全面扫描
  5. 打开计算机窗口的快捷键是什么,快速切换窗口的快捷键是什么
  6. 求生之路2 服务器显示人满,求生之路2服务器怎么设置人数
  7. Linux环境下Shell脚本基础篇-鸡兔同笼问题
  8. Ubuntu加装4T机械硬盘
  9. MSQL常见面试问题
  10. MFC 对话框打印和打印预览知识总结